Golang’s Channel is a key component of its CSP concurrency model and a bridge for communication between Goroutines. Channel is frequently used in Golang, and it is crucial to have a deep understanding of its internal implementation principles. This article will analyze the underlying implementation of Channel based on the Go 1.13 source code.
Before formally analyzing the implementation of Channel, let’s review its basic usage:
<code class="language-go">package main import "fmt" func main() { c := make(chan int) go func() { c <- 1 // 发送操作 }() x := <-c // 接收操作 fmt.Println(x) }</code>
This code shows two basic operations of Channel:
c <- 1
x := <-c
Channel is divided into buffered Channel and non-buffered Channel. The above code uses a non-buffered Channel. In a non-buffered Channel, if no other Goroutine is currently receiving data, the sender will block at the send statement.
You can specify the buffer size when initializing the Channel. For example, make(chan int, 2)
specifies the buffer size to be 2. Before the buffer is full, the sender can send data without blocking without waiting for the receiver to be ready. But if the buffer is full, the sender will still block.
Before diving into the Channel source code, you need to find the specific implementation location of Channel in Golang. When using Channel, the underlying functions such as runtime.makechan
, runtime.chansend
and runtime.chanrecv
are actually called.
You can use the go tool compile -N -l -S hello.go
command to convert the code into assembly instructions, or use the online tool Compiler Explorer (for example: go.godbolt.org/z/3xw5Cj). By analyzing the assembly instructions, we can find:
make(chan int)
corresponds to the runtime.makechan
function. c <- 1
corresponds to the runtime.chansend
function. x := <-c
corresponds to the runtime.chanrecv
function. The implementation of these functions are located in the runtime/chan.go
file of the Go source code.
make(chan int)
will be converted into a runtime.makechan
function by the compiler, and its function signature is as follows:
<code class="language-go">func makechan(t *chantype, size int) *hchan</code>
Among them, t *chantype
is the Channel element type, size int
is the user-specified buffer size (0 if not specified), and the return value is *hchan
. hchan
is the internal implementation structure of Channel in Golang, defined as follows:
<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>
The attributes in hchan
are roughly divided into three categories:
buf
, dataqsiz
, qcount
, etc. When the buffer size of the Channel is not 0, the buffer is used to store the data to be received, and is implemented using a ring buffer. recvq
contains Goroutine waiting to receive data, sendq
contains Goroutine waiting to send data. waitq
Implemented using a doubly linked list. lock
, elemtype
, closed
, etc. makechan
function mainly performs some legality checks and memory allocation of attributes such as buffers and hchan
, which will not be discussed in depth here.
Based on a simple analysis of the hchan
attribute, it can be seen that there are two important components: buffer and waiting queue. All behaviors and implementations of hchan
revolve around these two components.
The sending and receiving processes of Channel are very similar. First analyze the sending process of Channel (for example c <- 1
).
tries to send data to the Channel, if the recvq
queue is not empty, a Goroutine waiting to receive data will be taken out from the recvq
header and the data will be sent directly to the Goroutine. The code is as follows:
<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
Contains Goroutine waiting to receive data. When a Goroutine uses a receive operation (such as x := <-c
), if sendq
is not empty at this time, a Goroutine will be taken from sendq
and the data will be sent to it.
If recvq
is empty, it means that there is no Goroutine waiting to receive data at this time, and the Channel will try to put the data into the buffer:
<code class="language-go">func makechan(t *chantype, size int) *hchan</code>
The function of this code is very simple, it is to put data into the buffer. This process involves the operation of a ring buffer, dataqsiz
represents the user-specified buffer size (defaults to 0 if not specified).
If a non-buffered Channel is used or the buffer is full (c.qcount == c.dataqsiz
), the data to be sent and the current Goroutine will be packaged into a sudog
object, placed in sendq
, and the current Goroutine will be set to wait Status:
<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
will unlock the input mutex and suspend the current Goroutine, setting it to a wait state. gopark
and goready
appear in pairs and are reciprocal operations.
From the user's perspective, after calling gopark
, the code statement for sending data will block.
The receiving process of Channel is basically similar to the sending process, so I won’t go into details here. The buffer-related operations involved in the reception process will be described in detail later.
It should be noted that the entire sending and receiving process of Channel is locked using runtime.mutex
. runtime.mutex
is a lightweight lock commonly used in runtime-related source code. The whole process is not the most efficient lock-free solution. There is an issue about lock-free Channel in Golang: go/issues#8899.
Channel uses a ring buffer to cache written data. Ring buffers have many advantages and are ideal for implementing fixed-length FIFO queues.
The implementation of the ring buffer in Channel is as follows:
There are two buffer-related variables inhchan
: recvx
and sendx
. sendx
represents a writable index in the buffer, and recvx
represents a readable index in the buffer. Elements between recvx
and sendx
represent data that has been put into the buffer normally.
You can directly use buf[recvx]
to read the first element of the queue, and use buf[sendx] = x
to put the element at the end of the queue.
When the buffer is not full, the operation of putting data into the buffer is as follows:
<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)
is equivalent to c.buf[c.sendx]
. The above process is very simple, just copy the data to the buffer location sendx
.
Then, move sendx
to the next position. If sendx
reaches the last position, it is set to 0, which is a typical end-to-end approach.
When the buffer is not full, sendq
must also be empty (because if the buffer is not full, the Goroutine sending the data will not be queued, but will directly put the data into the buffer). At this time, the reading logic of Channel chanrecv
is relatively simple. Data can be read directly from the buffer. It is also a process of moving recvx
, which is basically the same as the buffer writing above.
When there is a waiting Goroutine in sendq
, the buffer must be full at this time. At this time, the reading logic of Channel is as follows:
<code class="language-go">func makechan(t *chantype, size int) *hchan</code>
ep
is the address corresponding to the variable that receives the data (for example, in x := <-c
, ep
is the address of x
). sg
represents the first sendq
taken from sudog
. In the code:
typedmemmove(c.elemtype, ep, qp)
means copying the currently readable element in the buffer to the address of the receiving variable. typedmemmove(c.elemtype, qp, sg.elem)
means copying the data waiting to be sent by Goroutine in sendq
to the buffer. Because recv
is executed later, it is equivalent to placing the data in sendq
at the end of the queue. Simply put, here Channel copies the first data in the buffer to the corresponding receiving variable, and at the same time copies the elements in sendq
to the end of the queue, thereby implementing FIFO (first in, first out).
Channel is one of the most commonly used facilities in Golang. Understanding its source code will help you better use and understand Channel. At the same time, do not be overly superstitious and rely on the performance of Channel. The current design of Channel still has a lot of room for optimization.
Optimization suggestions:
Finally, I recommend a platform that is very suitable for deploying Go services: Leapcell
Please check the documentation for more information!
Leapcell Twitter: https://www.php.cn/link/7884effb9452a6d7a7a79499ef854afd
The above is the detailed content of Go Channel Unlocked: How They Work. For more information, please follow other related articles on the PHP Chinese website!