Golang의 채널은 CSP 동시성 모델의 핵심 구성 요소이자 Goroutine 간의 통신을 위한 브리지입니다. 채널은 Golang에서 자주 사용되며 내부 구현 원칙을 깊이 이해하는 것이 중요합니다. 이 기사에서는 Go 1.13 소스 코드를 기반으로 Channel의 기본 구현을 분석합니다.
채널 구현을 공식적으로 분석하기 전에 기본 사용법을 검토해 보겠습니다.
<code class="language-go">package main import "fmt" func main() { c := make(chan int) go func() { c <- 1 // 发送操作 }() x := <-c // 接收操作 fmt.Println(x) }</code>
이 코드는 채널의 두 가지 기본 작업을 보여줍니다.
c <- 1
x := <-c
채널은 버퍼링된 채널과 버퍼링되지 않은 채널로 구분됩니다. 위의 코드는 버퍼링되지 않은 채널을 사용합니다. 버퍼링되지 않은 채널에서 다른 고루틴이 현재 데이터를 수신하고 있지 않으면 발신자는 send 문에서 차단됩니다.
채널을 초기화할 때 버퍼 크기를 지정할 수 있습니다. 예를 들어 make(chan int, 2)
에서는 버퍼 크기를 2로 지정합니다. 버퍼가 가득 차기 전에 송신자는 수신자가 준비될 때까지 기다리지 않고 차단하지 않고 데이터를 보낼 수 있습니다. 그러나 버퍼가 가득 차면 발신자는 여전히 차단됩니다.
채널 소스 코드를 살펴보기 전에 Golang에서 채널의 구체적인 구현 위치를 찾아야 합니다. 채널을 사용하면 runtime.makechan
, runtime.chansend
, runtime.chanrecv
등의 기본 함수가 실제로 호출됩니다.
go tool compile -N -l -S hello.go
명령을 사용하여 코드를 어셈블리 지침으로 변환하거나 온라인 도구 컴파일러 탐색기(예: go.godbolt.org/z/3xw5Cj)를 사용할 수 있습니다. 조립 지침을 분석하여 다음을 찾을 수 있습니다.
make(chan int)
은 runtime.makechan
기능에 해당합니다. c <- 1
은 runtime.chansend
기능에 해당합니다. x := <-c
은 runtime.chanrecv
기능에 해당합니다. 이러한 기능의 구현은 Go 소스 코드의 runtime/chan.go
파일에 있습니다.
make(chan int)
은 컴파일러에 의해 runtime.makechan
함수로 변환되며 해당 함수 서명은 다음과 같습니다.
<code class="language-go">func makechan(t *chantype, size int) *hchan</code>
그 중 t *chantype
은 채널 요소 유형이고, size int
은 사용자가 지정한 버퍼 크기(지정하지 않은 경우 0)이며 반환 값은 *hchan
입니다. hchan
은 Golang의 Channel 내부 구현 구조로 다음과 같이 정의됩니다.
<code class="language-go">type hchan struct { qcount uint // 缓冲区中已放入元素的数量 dataqsiz uint // 用户构造Channel时指定的缓冲区大小 buf unsafe.Pointer // 缓冲区 elemsize uint16 // 缓冲区中每个元素的大小 closed uint32 // Channel是否关闭,==0表示未关闭 elemtype *_type // Channel元素的类型信息 sendx uint // 缓冲区中发送元素的索引位置(发送索引) recvx uint // 缓冲区中接收元素的索引位置(接收索引) recvq waitq // 等待接收的Goroutine列表 sendq waitq // 等待发送的Goroutine列表 lock mutex }</code>
hchan
의 속성은 대략 세 가지 범주로 나뉩니다.
buf
, dataqsiz
, qcount
등 Channel의 버퍼 크기가 0이 아닌 경우, 수신할 데이터를 저장하는 버퍼로 사용되며, 링 버퍼를 이용하여 구현된다. recvq
에는 데이터 수신을 기다리는 고루틴이 포함되고, sendq
에는 데이터 전송을 기다리는 고루틴이 포함됩니다. waitq
이중 연결 리스트를 사용하여 구현되었습니다. lock
, elemtype
, closed
등 makechan
함수는 주로 일부 합법성 검사와 버퍼 및 hchan
과 같은 속성의 메모리 할당을 수행하는데, 이에 대해서는 여기서 자세히 설명하지 않습니다.
hchan
속성을 간단히 분석해 보면 버퍼와 대기 큐라는 두 가지 중요한 구성 요소가 있음을 알 수 있습니다. hchan
의 모든 동작과 구현은 이 두 구성 요소를 중심으로 이루어집니다.
채널의 전송 및 수신 프로세스는 매우 유사합니다. 먼저 채널(예: c <- 1
)의 전송 프로세스를 분석합니다.
가 채널에 데이터를 보내려고 할 때 recvq
대기열이 비어 있지 않으면 데이터 수신을 기다리고 있는 고루틴이 recvq
헤더에서 제거되고 데이터가 고루틴으로 직접 전송됩니다. 코드는 다음과 같습니다.
<code class="language-go">package main import "fmt" func main() { c := make(chan int) go func() { c <- 1 // 发送操作 }() x := <-c // 接收操作 fmt.Println(x) }</code>
recvq
데이터 수신을 기다리는 고루틴이 포함되어 있습니다. 고루틴이 수신 작업(예: x := <-c
)을 사용할 때 sendq
가 이때 비어 있지 않으면 sendq
에서 고루틴을 가져오고 데이터가 해당 고루틴으로 전송됩니다.
recvq
이 비어 있으면 현재 데이터 수신을 기다리고 있는 고루틴이 없으며 채널이 데이터를 버퍼에 넣으려고 시도한다는 의미입니다.
<code class="language-go">func makechan(t *chantype, size int) *hchan</code>
이 코드의 기능은 매우 간단합니다. 데이터를 버퍼에 넣는 것입니다. 이 프로세스에는 링 버퍼의 작업이 포함되며, dataqsiz
은 사용자가 지정한 버퍼 크기를 나타냅니다(지정하지 않은 경우 기본값은 0).
버퍼되지 않은 채널을 사용하거나 버퍼가 가득 찬 경우(c.qcount == c.dataqsiz
), 전송할 데이터와 현재 고루틴은 sudog
객체로 패키징되어 sendq
에 배치되고, 현재 고루틴은 대기 상태로 설정됩니다:
<code class="language-go">type hchan struct { qcount uint // 缓冲区中已放入元素的数量 dataqsiz uint // 用户构造Channel时指定的缓冲区大小 buf unsafe.Pointer // 缓冲区 elemsize uint16 // 缓冲区中每个元素的大小 closed uint32 // Channel是否关闭,==0表示未关闭 elemtype *_type // Channel元素的类型信息 sendx uint // 缓冲区中发送元素的索引位置(发送索引) recvx uint // 缓冲区中接收元素的索引位置(接收索引) recvq waitq // 等待接收的Goroutine列表 sendq waitq // 等待发送的Goroutine列表 lock mutex }</code>
goparkunlock
은 입력 뮤텍스를 잠금 해제하고 현재 고루틴을 일시 중지하여 대기 상태로 설정합니다. gopark
과 goready
은 쌍으로 나타나며 상호 연산입니다.
사용자 입장에서는 gopark
호출 후 데이터 전송을 위한 코드문이 차단됩니다.
채널의 수신 과정은 기본적으로 전송 과정과 유사하므로 여기서는 자세히 설명하지 않겠습니다. 수신 과정에서 발생하는 버퍼 관련 동작에 대해서는 뒤에서 자세히 설명한다.
runtime.mutex
을 사용하면 채널의 전체 송수신 과정이 잠겨 있으므로 주의해야 합니다. runtime.mutex
은 런타임 관련 소스 코드에서 일반적으로 사용되는 경량 잠금입니다. 전체 프로세스가 가장 효율적인 잠금 없는 솔루션은 아닙니다. Golang의 잠금 없는 채널에 관한 문제가 있습니다: go/issues#8899.
채널은 링 버퍼를 사용하여 작성된 데이터를 캐시합니다. 링 버퍼에는 많은 장점이 있으며 고정 길이 FIFO 대기열을 구현하는 데 이상적입니다.
채널의 링 버퍼 구현은 다음과 같습니다.
hchan
에는 버퍼 관련 변수가 두 개 있는데, recvx
과 sendx
입니다. sendx
은 버퍼에 쓰기 가능한 인덱스를 나타내고, recvx
는 버퍼에 읽기 가능한 인덱스를 나타냅니다. recvx
과 sendx
사이의 요소는 정상적으로 버퍼에 담긴 데이터를 나타냅니다.
buf[recvx]
을 사용하여 큐의 첫 번째 요소를 직접 읽을 수 있고, buf[sendx] = x
을 사용하여 해당 요소를 큐의 끝에 넣을 수 있습니다.
버퍼가 가득 차지 않은 경우, 버퍼에 데이터를 넣는 동작은 다음과 같습니다.
<code class="language-go">package main import "fmt" func main() { c := make(chan int) go func() { c <- 1 // 发送操作 }() x := <-c // 接收操作 fmt.Println(x) }</code>
chanbuf(c, c.sendx)
은 c.buf[c.sendx]
과 동일합니다. 위의 과정은 매우 간단합니다. 데이터를 버퍼 위치sendx
에 복사하기만 하면 됩니다.
그런 다음 sendx
을 다음 위치로 이동하세요. sendx
이 마지막 위치에 도달하면 0으로 설정되는데, 이는 일반적인 end-to-end 접근 방식입니다.
버퍼가 가득 차지 않으면 sendq
도 비어 있어야 합니다(왜냐하면 버퍼가 가득 차지 않으면 데이터를 보내는 고루틴이 대기열에 들어가지 않고 데이터를 직접 버퍼에 넣기 때문입니다). 이때 채널 chanrecv
의 읽기 로직은 비교적 간단합니다. 버퍼에서 직접 데이터를 읽을 수도 있으며, 이는 기본적으로 위의 버퍼 쓰기와 동일합니다. recvx
에 대기 중인 고루틴이 있으면 이때 버퍼가 가득 차 있어야 합니다. 이때 채널의 읽기 로직은 다음과 같습니다. sendq
<code class="language-go">func makechan(t *chantype, size int) *hchan</code>
은 데이터를 받는 변수에 해당하는 주소입니다(예를 들어 ep
에서 x := <-c
은 ep
의 주소입니다). x
은 sg
에서 가져온 첫 번째 sendq
를 나타냅니다. 코드에서: sudog
typedmemmove(c.elemtype, ep, qp)
typedmemmove(c.elemtype, qp, sg.elem)
에서 고루틴이 전송하기를 기다리는 데이터를 버퍼에 복사하는 것을 의미합니다. sendq
은 나중에 실행되기 때문에 대기열 끝에 recv
에 데이터를 배치하는 것과 같습니다. sendq
의 요소를 대기열 끝에 복사하여 FIFO(선입선출)를 구현합니다. . sendq
채널은 Golang에서 가장 일반적으로 사용되는 기능 중 하나입니다. 소스 코드를 이해하면 채널을 더 잘 사용하고 이해하는 데 도움이 됩니다. 동시에 지나치게 미신을 믿지 말고 채널의 성능에 의존하지 마십시오. 현재 채널의 디자인에는 여전히 최적화할 여지가 많습니다.
최적화 제안:
마지막으로 Go 서비스 배포에 매우 적합한 플랫폼을 추천합니다: Leapcell
자세한 내용은 설명서를 확인하세요!
Leapcell 트위터: https://www.php.cn/link/7884effb9452a6d7a7a79499ef854afd
위 내용은 Go 채널 잠금 해제: 작동 방식의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!