本文基于go版本1.16
数据结构
其底层数据结构为runtime包下的一个hchan的结构体,如下:
1 | type hchan struct { |
buf
指向底层循环数组,只有缓冲型的channel才有sendx, recvx
均指向底层循环数组,表示当前可以发送和接收的元素位置索引值(相对于底层数组)sendq, recvq
分别表示向channel读取或发送数据而阻塞的goroutine队列waitq
是sudog的一个双向链表(sudog实际上是对goroutine的封装)lock
用来保证每个读channel或写channel的操作都是原子的
创建
使用make
能创建一个能收能发的channel:
1 | // 无缓冲通道 |
通过汇编分析(go complie),找到最终创建chan的函数是位于runtime/chan.go下的函数makechan
:
1 | const ( |
发送
1 | func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { |
1 | func full(c *hchan) bool { |
这里有一个点需要注意,结合在
chansend
中调用full
的地方上的英文注释进行分析,在得知channel未被关闭的情况下(c.closed==0
),去获取c.recvq.first
和c.qcount
的值时为什么不需要加锁(假设这个期间channe被关闭,则前后条件实际上是不一致的)?
- 因为一个已经关闭的channel不能将channel状态从
ready for sending
变成not ready for sending
,意味着在两个观测之间有一个时刻,通道既没有被关闭,也没有准备好发送,此时直接返回false也是没有问题的- 然后其会依赖
chanrecv()
和closechan()
中锁释放的副作用来更新这个线程的c.closed
和full
如果从等待接收队列recvq里出队一个sudog(代表一个goroutine),说明此时channel是空的,没有元素,所以才会有等待接收者。这时会调用send函数将元素直接从发送者的栈拷贝到接收者的栈,关键操作由sendDirect
函数完成。
1 | // send processes a send operation on an empty channel c. |
继续看sendDirect
函数:
1 | // Sends and receives on unbuffered or empty-buffered channels are the |
这里涉及到一个goroutine直接写另一个goroutine栈的操作,一般而言,不同goroutine的栈是各自独有的。而这也违反了GC的一些假设。为了不出问题,写的过程中增加了写屏障,保证正确的完成写操作。这样做的好处是减少了一次内存拷贝,不用先拷贝到channel的buf,直接由发送者到接收者,减少了中间一层,效率得以提高。然后解锁,唤醒接收者,等待调度器的光临,接收者得以重见天日,可以继续执行接收操作后续代码了。
接收
接收操作有两种写法,一种带”ok”,表示channel是否被关闭;一种不带”ok”,这种写法当接收到相应类型的零值时无法知道是真实的发送者发送者发送过来的值,还是channel被关闭后,返回给接收者默认类型的零值。经过编译器的处理后,这两种写法对应源码以下的两个函数:
1 | // entry points for <- c from compiled code |
有上述源码可见,最终都会调用chanrecv
这个函数:
1 | // chanrecv receives on channel c and writes the received data to ep. |
关闭
1 | func closechan(c *hchan) { |
- close逻辑比较简单,对于一个channel,recvq和sendq中分别保存了阻塞的发送者和接收者。关闭channel后,对于等待接收者而言,会收到一个相应类型的零值。对于等待发送者,会直接panic。所以,在不了解channel还有没有接收者的情况下,不能贸然关闭channel。
- close函数先上一把大锁,接着把所有挂在这个channel上的sender和receiver全都连成一个sudog链表,再解锁。最后再将所有的sudog全都唤醒。
- 唤醒之后,该干什么干什么。sender会继续执行chansend函数里goparkunlock函数之后的代码,当检测到channel已经关闭了则会panic。receiver则会进行后续的扫尾工作,然后返回,这里selected会返回true,received会根据channel是否关闭返回不同的值。