Channel is a communication method between goroutines provided by Golang, which allows one goroutine to send a specific value to another goroutine.
Features
When the channel does not have a buffer, or there is a buffer but there is no data in the buffer, reading data from the channel will block until a coroutine writes data to the channel. Similarly, when the channel does not have a buffer, or when the buffer is full, writing data to the channel will also block until a coroutine reads data from the channel. For a channel with a value of nil, both read and write will be blocked, and it is permanently blocked.
Use the built-in function close to close the channel. Trying to send data to the closed channel will trigger the panic, but it is still readable at this time. The expression read by the channel has at most two return values:
x, ok := <-ch
The first variable indicates the data read, and the second variable indicates whether the data is successfully read. Its value is only related to whether there is data in the channel buffer, and has nothing to do with the closed state of the channel.
Implementation Principle
data structure
The source code src/runtime/chan.go:hchan defines the data structure of the channel:
type hchan struct {
qcount uint // 当前队列中剩余元素个数
dataqsiz uint // 环形队列长度,即可以存放的元素个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 每个元素的大小
closed uint32 // 标识关闭状态
elemtype *_type // 元素类型
sendx uint // 队列下标,指示元素写入时存放到队列中的位置
recvx uint // 队列下标,指示元素从队列的该位置读出
recvq waitq // 等待读消息的 goroutine 队列
sendq waitq // 等待写消息的 goroutine 队列
lock mutex // 互斥锁,chan 不允许并发读写
}
It can be seen that the channel is composed of queues, type information, and goroutine waiting queues.
circular queue
The channel implements a circular queue as its buffer, and the length of the queue is specified when the channel is created. The following figure shows a schematic diagram of a channel that can cache 6 elements:
- dataqsiz indicates that the queue length is 6, which can cache 6 elements;
- buf points to the memory address of the queue;
- qcount means there are two more elements in the queue;
- sendx represents the storage location of the subsequently written data, the value is [0, 6);
- recvx represents the location of the read data, the value is [0, 6).
Type information
A channel can only pass one type of value:
- elemtype represents the type, used for assignment in the data transfer process;
- elemsize represents the type size, used to locate the element position in buf.
waiting queue
When reading data from the channel, if there is no buffer or the buffer is empty, the current coroutine will be blocked and added to the recvq queue. When writing data to the channel, if there is no buffer or the buffer is full, the current coroutine will also be blocked, and then added to the sendq queue. The coroutines in the waiting queue will be awakened when other coroutines operate the channel.
The following figure shows a channel without a buffer, and several coroutines are blocking waiting to read data:
Related operations
Create channel
The process of creating a channel is actually to initialize the hchan structure. The type information and buffer length are passed in by the make statement, and the size of buf is determined by the element size and buffer length.
The source code src/runtime/chan.go defines the function makechan() to create a channel. The simplified version of the code is as follows:
func makechan(t *chantype, size int) *hchan {
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
var c *hchan
switch {
case mem == 0:
c = (*hchan)(mallocgc(hchanSize, nil, true))
c.buf = c.raceaddr()
case elem.ptrdata == 0:
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
return c
}
Send data
The operation of sending data is finally transformed into the chansend() function. The main code and logic are as follows:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 如果通道为 nil,非阻塞式发送的话直接返回 false,否则将当前协程挂起
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 对于非阻塞式发送,如果通道未关闭且没有缓冲空间的话,直接返回 false
if !block && c.closed == 0 && full(c) {
return false
}
// 加锁,并发安全
lock(&c.lock)
// 如果通道关闭了,直接 panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// 如果接收队列不为空,直接将要发送的数据发送到队首的 goroutine
if sg := c.recvq.dequeue(); sg != nil {
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
// 对于缓冲区还有空闲的 channel,拷贝数据到缓冲区,维护相关信息
if c.qcount < c.dataqsiz {
qp := chanbuf(c, c.sendx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
}
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
// 没有缓冲空间时,发送方会挂起,并根据当前 goroutine 构造一个 sudog 结构体添加到 sendq 队列中
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
// 省略被唤醒时部分代码
return true
}
Read data
The operation of reading data is finally transformed into the chanrecv() function, and the main logic is as follows:
// selected 和 received 返回值分别代表是否可被 select 语句命中以及是否读取到了数据
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// 如果 channel 为 nil,非阻塞式读取直接返回,否则直接挂起
if c == nil {
if !block {
return
}
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 非阻塞模式并且没有消息可读(没有缓冲区或者缓冲区为空),如果 channel 未关闭直接返回
if !block && empty(c) {
if atomic.Load(&c.closed) == 0 {
return
}
if empty(c) {
if raceenabled {
raceacquire(c.raceaddr())
}
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
}
// 加锁
lock(&c.lock)
// channel 已关闭并且没有消息可读(没有缓冲区或者缓冲区为空),会接收到零值,typedmemclr 会根据类型清理相应地址的内存
if c.closed != 0 && c.qcount == 0 {
if raceenabled {
raceacquire(c.raceaddr())
}
unlock(&c.lock)
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
// 等待发送队列不为空,如果是非缓冲型 channel,直接拷贝发送者的数据,否则接收队首的数据,并将发送者的数据移动到环形队列尾部
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
// 缓冲型 channel,buf 里有元素,可以正常接收
if c.qcount > 0 {
// Receive directly from queue
qp := chanbuf(c, c.recvx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
}
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
typedmemclr(c.elemtype, qp)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
unlock(&c.lock)
return true, true
}
// 被阻塞的情况,构造一个 sudog 结构体,保存到 channel 的等待接收队列,并将当前 goroutine 挂起
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.param = nil
c.recvq.enqueue(mysg)
atomic.Store8(&gp.parkingOnChan, 1)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
// 省略被唤醒时部分代码
return true, !closed
}
Close the channel
Close a channel, and finally execute the function closechan(), the core code is as follows:
func closechan(c *hchan) {
// 如果 channel 为 nil,直接 panic
if c == nil {
panic(plainError("close of nil channel"))
}
// 加锁,如果 channel 已关闭,直接 panic
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
c.closed = 1
var glist gList
// 释放等待接收队列中,向需要返回值的接收者返回相应的零值
for {
sg := c.recvq.dequeue()
if sg == nil {
break
}
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
}
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = nil
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
// 释放等待发送队列,相关的 goroutine 会触发panic
for {
sg := c.sendq.dequeue()
if sg == nil {
break
}
sg.elem = nil
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = nil
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
unlock(&c.lock)
// ...
}
Common applications
Timed task
This usage needs to be combined with the timer, which is divided into two types: timeout control and timing execution.
If you need to perform an operation, but don't want it to take too long, and want to give it a timeout limit, you can do this:
select {
case <-time.After(100 * time.Millisecond):
case <-s.stopc:
return false
}
After waiting for 100 ms, if s.stopc has not read data or is closed, it ends directly.
It is also relatively simple to execute a certain task on a regular basis, for example, every 1 second, execute a regular task:
func worker() {
ticker := time.Tick(1 * time.Second)
for {
select {
case <- ticker:
// 执行任务
}
}
}
Decoupling producers and consumers
Use a channel to save tasks, start n goroutines as a pool of work coroutines, these coroutines work in an infinite loop, read tasks from the channel and execute:
func main() {
taskCh := make(chan int, 100)
go worker(taskCh)
for i := 0; i < 10; i++ {
taskCh <- i
}
select {
case <-time.After(time.Hour):
}
}
func worker(taskCh <-chan int) {
const N = 5
for i := 0; i < N; i++ {
go func(id int) {
for {
task := <- taskCh
fmt.Printf("finish task: %d by worker %d\n", task, id)
time.Sleep(time.Second)
}
}(i)
}
}
control the number of concurrent
Sometimes hundreds of tasks need to be executed regularly, but the number of concurrency cannot be too high. At this time, the number of concurrency can be controlled through the channel. For example, the following example:
var limit = make(chan int, 3)
func main() {
// …………
for _, w := range work {
go func() {
limit <- 1
w()
<-limit
}()
}
// …………
}
Construct a channel with a capacity of 3, traverse the task list, and start a goroutine for each task. The actual execution of the task is done in w(). Before executing w(), you must first get the "license" from the limit. After you get the license, you can execute w(), and you must return the "license" after performing the task. It should be noted that if w() panic occurs, the "license" may not go back yet, so you need to use defer to ensure it.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。