Golang中Channel是goroutine间重要通信的方式,是并发安全的,通道内的数据First In First Out,我们可以把通道想象成队列。这里面分析的源码基于go1.13版本。
channel数据结构
Channel底层数据结构是一个结构体。
1 | type hchan struct { |
hchan结构体中的buf指向一个数组,用来实现循环队列,sendx是循环队列的队尾指针,recvx是循环队列的队头指针。dataqsize是缓存型通道的大小,qcount是记录通道内元素个数。
循环队列一般使用空余单元法来解决队空和队满时候都存在font=rear带来的二义性问题,但这样会浪费一个单元。golang的channel中是通过增加qcount字段记录队列长度来解决二义性,一方面不会浪费一个存储单元,另一方面当使用len函数查看队列长度时候,可以直接返回qcount字段,一举两得。
hchan结构体中另一重要部分是recvq,sendq,分别存储了等待从通道中接收数据的goroutine,和等待发送数据到通道的goroutine。两者都是waitq类型。
waitq是一个结构体类型,waitq和sudog构成双向链表,其中sudog是链表元素的类型,waitq中first和last字段分别指向链表头部的sudog,链表尾部的sudog。
1 | type waitq struct { |
hchan结构图如下:
channel的创建
在分析channel的创建代码之前,我们看下源码文件中最开始定义的两个常量;
1 | const ( |
- maxAlgin用来设置内存最大对齐值,对应就是64位系统下cache line的大小。当结构体是8字节对齐时候,能够避免false share,提高读写速度
- hchanSize用来设置chan大小,unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1)),这个复杂公式用来计算离unsafe.Sizeof(hchan{})最近的8的倍数。假设hchan{}大小是13,hchanSize是16。
假设n代表unsafe.Sizeof(hchan{}),a代表maxAlign,c代表hchanSize,则上面hchanSize的计算公式可以抽象为:
c = n + ((-n) & (a - 1))
计算离8最近的倍数,只需将n补足与到8倍数的差值就可,c也可以用下面公式计算
c = n + (a - n%a)
感兴趣的可以证明在a为2的n的次幂时候,上面两个公式是相等的。
1 | func makechan(t *chantype, size int) *hchan { |
发送数据到channel
1 | func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { |
从channel中读取数据
1 | func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) { |
关闭channel
1 | func closechan(c *hchan) { |
总结
- channel规则:
操作 | 空Channel | 已关闭Channel | 活跃Channel |
---|---|---|---|
close(ch) | panic | panic | 成功关闭 |
ch <-v | 永远阻塞 | panic | 成功发送或阻塞 |
v,ok = <-ch | 永远阻塞 | 不阻塞 | 成功接收或阻塞 |
注意: 从空通道中写入或读取数据会永远阻塞,这会造成goroutine泄漏。
- 发送、接收数据以及关闭通道流程图: