引言
channel 是 golang 的最重要的一个结构,是区别于其他高级语言的最重要的特色之一,也是 goroutine 通信必须要的要素之一。下文将基于golang1.14从channel的数据结构&收、发操作的代码实现,进一步了解channel。
hchan struct
hchan 中的所有属性大致可以分为三类:
- buffer 相关的属性。例如 buf、dataqsiz、qcount 等。 当 channel 的缓冲区大小不为 0 时,buffer 中存放了待接收的数据。使用 ==环形队列==(ring buffer) 实现,FIFO。
- waitq 相关的属性,可以理解为是一个 FIFO 的标准队列。其中 recvq 中是正在等待接收数据的 goroutine,sendq 中是等待发送数据的 goroutine。waitq 使用==双向链表==实现。
- 其他属性,例如 lock、elemtype、closed 等。
1 | type hchan struct { |
我们可以看到
- channel 其实就是一个队列加一个锁,只不过这个锁是一个轻量级锁。
- recvq 是读操作阻塞在 channel 的 goroutine 列表,sendq 是写操作阻塞在 channel 的 goroutine 列表。
- 链表的实现是 sudog,其实就是一个对 g 的结构的封装。
makechan
- 参数校验 2-15行
- 初始化hchan 17-37行
1 | func makechan(t *chantype, size int) *hchan { |
chansend
chansend 函数是在编译器解析到 c <- x
这样的代码的时候插入的,本质上就是把一个用户元素投递到 hchan 的 ringbuffer 中。chansend 调用的时候,一般用户会遇到三种情况:
- 投递成功,非常顺利,正常返回true
- 投递受阻,该函数阻塞,goroutine 切走
- 投递失败返回false
1 | func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { |
golang内执行 ==<- x== 会调用chansend函数,会有三种场景:
场景一:如果有人( goroutine )等着取 channel 的元素,这种场景最快乐,直接把元素给他就完了,然后把它唤醒,hchan 本身递增下 ringbuffer 索引;举一反三:kafka也有这种高效操作。
场景二:如果 ringbuffer 还有空间,那么就把元素存着,这种也是场景的流程,存和取走的是异步流程,可以把 channel 理解成消息队列,生产者和消费者解耦;
场景三:ringbuffer 没空间,这个时候就要是否需要 block 了,一般来讲,
c <- x
编译出的代码都是block = true
,那么什么时候 chansend 的 block 参数会是 false 呢?答案是:select 的时候;
1 | select { |
chanrecv
chanrecv 函数是在编译器解析到 <- c
这样的代码的时候插入的,本质上就是从sender或 hchan 的 ringbuffer 中取一个元素。chanrecv 调用的时候,一般用户会遇到三种情况:
- 接收成功,非常顺利,正常返回元素,true
- 接收受阻,该函数阻塞,goroutine 切走
- 接收失败返回nil,false
1 | // <- c 对应 |
可以看到send和recv代码和处理情况基本一样:
- 如果是非阻塞模式( block=false ),并且没有任何可用元素,返回 (selected=false,received=false),这样就不会进到 select 的 case 分支;
- 如果是阻塞模式( block=true ),如果 chan 已经 closed 了,那么返回的是 (selected=true,received=false),说明需要进到 select 的分支,但是是没有取到元素的;
- 如果是阻塞模式,chan 还是正常状态,那么返回(selected=true,recived=true),说明正常取到了元素;
select部分和send基本一致,在编译时block参数设为false,不再重复。不过recv还可以通过range的方式进行。
for循环
for-range
和 chan 的结束条件只有这个 chan 被 close 了,否则一直会处于这个死循环内部,因为block参数为true。1
2
3
4
5
6
7
8
9
10
11
12
13
14for m := range c {
// ... do something
}
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
// 注意了,这个 block=true,说明 chanrecv 内部是阻塞的;
_, received = chanrecv(c, elem, true)
return
}
// 伪代码
for ( ; ok = chanrecv2( c, ep ) ; ) {
// do something
}
参考
http://legendtkl.com/2017/08/06/golang-channel-implement/