golang系列之-channel
channel-管道,是go语言中一种常见的goroutine的通信方式
当前go版本:1.24
快速上手
示例1. 两个goroutine之间使用channel传递数据
1 | message := make(chan string) |
示例2. 使用select同时监听多个goroutine的响应数据,实际上,业务代码中一般都是跟定时器搭配使用
1 | ch1 := make(chan int) |
数据结构
1 | type hchan struct { |
创建channel
创建channel,具体逻辑如下
- guard,让错误尽早返回
- 计算创建channel所需的内存大小(header+buf)
- 创建channel、初始化字段数据
1 | func makechan64(t *chantype, size int64) *hchan { |
发送数据
如何发送数据到channel?当使用代码c <- x
时,系统将编译为对chansend1的调用;当使用select发送数据时,编译为对selectnbsend的调用;而这两个函数最终会调用chansend
1 | // `c <- x` |
chansend代码如下,具体逻辑为
- select场景,非阻塞
- 如果channel 已关闭,异常
- 如果channel 未初始化或buf已满,发送失败,返回
- c <- v场景,阻塞
- 如果channel 未初始化或已关闭,异常
- 共同逻辑
- 加锁double-check,如果channel 已关闭,异常
- 如果已经有读G在等待,说明buf为空,把数据给队列的第一个读G并唤醒,返回
- buf未满,写入下一个空位置,更新索引、计数器,返回
- buf已满,非阻塞返回写入失败,阻塞则把当前G封装到sudog放进写队列,挂起等待
- 被唤醒后
- 如果是因为channel 被关闭导致的唤醒,异常
- 数据已被读G拿走,清理收尾
1 | // 传递数据到channel |
接收数据
如何从channel接收数据?当使用代码<- c
时,系统将根据返回值编译为对chanrecv1或chanrecv2的调用;当使用select接收数据时,编译为对selectnbrecv的调用;而这三个函数最终会调用chanrecv
1 | // `<- c` 或 `y := <- c` |
chanrecv代码如下,具体逻辑为
- select场景,非阻塞
- 如果 channel 未初始化或已关闭或buf为空,无数据,返回
- c <- v场景,阻塞
- 如果channel 未初始化,异常
- 共同逻辑
- 加锁double-check
- 如果channel 已关闭且buf为空,无数据,返回
- 如果已经有写G在等待,说明buf已满,读取队列的第一个写G并唤醒,返回
- buf不为空,读循环队列,更新索引、计数器,返回
- buf为空,非阻塞则返回读取失败,阻塞则把当前G封装到sudog放进读队列,挂起等待
- 数据已从写G读到,清理收尾
1 | func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) { |
关闭channel
关闭channel ,具体逻辑如下
- 如果channel 未初始化或已关闭,异常
- channel 设为已关闭closed=1
- 收集并唤醒所有在读写队列的G(写G会抛出异常)
1 | func closechan(c *hchan) { |
获取channel 数据量
当调用len(c)
时,系统调用chanlen实现,具体逻辑如下
- 未初始化的channel 数据量为0
- 如果是异步timer,返回qcount的数值
- 如果是同步timer,返回0
- 其他情况一律返回channel 字段qcount的数值
1 | func chanlen(c *hchan) int { |
获取channel 容量
当调用cap(c)
时,系统调用chancap实现,具体逻辑如下
- 未初始化的channel 容量为0
- 如果是异步timer,返回dataqsiz的数值
- 如果是同步timer,返回0
- 其他情况一律返回channel 字段dataqsiz的数值
1 | func chancap(c *hchan) int { |
定时器相关
maybeRunChan
判断是否需要更新timer状态、执行函数f。具体逻辑如下
- 不满足条件则返回
- timer已经放在最小堆上,那么过期后自动发送到channel
- timer从未执行过
- timer还未到触发时刻
- 满足条件则更新timer状态、执行函数f
1 | // 判断是否需要更新timer状态、执行函数f |
blockTimerChan & unblockTimerChan
1 | // 创建一个10ms过期的定时器 |
上述示例代码,如果这个channel是属于一个定时器的,那么在G挂起前、唤醒后,需要修改定时器的state-状态、blocked-标记等。函数详细注释如下
1 | // G挂起休眠之前,将定时器标记为阻塞并加入最小堆 |
参考文档
6.4 Channel
GopherCon 2017: Kavya Joshi - Understanding Channels
Diving Deep Into The Golang Channels.