本篇文章内容基于go1.14.2分析
golang的chan是一个内置类型,作为csp编程的核心数据结构,其底层数据结构是一个叫hchan的struct:
1 | go复制代码type hchan struct { |
如图所示,chan最核心的部分由一个环形队列和2个waitq组成,环形队列用于存放数据(带缓冲的情况下),waitq用于实现阻塞和恢复goroutine。
chan的相关操作
对chan的操作有:make、读、写、close,当然还有select,这里只讨论前面四个操作。
创建 chan
当在代码中使用make创建chan时,编译器会根据情况自动替换成makechan64 或者makechan,makechan64 其实还是调用了makechan函数。
1 | go复制代码func makechan(t *chantype, size int) *hchan { |
chan 写操作
当对chan进行写入“ch <- interface{}” 时,会被编译器替换成chansend1函数的调用,最终还是调用了chansend函数:
1 | go复制代码//elem 是待写入元素的地址 |
先看看chansend的函数签名,只需关注ep和block这个两个参数即可,ep是要写入数据的地址,block表示是否阻塞式的调用
1 | go复制代码func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool |
chansend有以下几种处理流程:
- 当对一个nil chan进行写操作时,如果是非阻塞调用,直接返回;否则将当前协程挂起
1 | go复制代码// chansend 对一个 nil chan发送数据时,如果是非阻塞则直接返回,否则将当前协程挂起 |
- 非阻塞模式且chan未close,没有缓冲区且没有等待接收或者缓冲区满的情况下,直接return false。
1 | go复制代码// 1. 非阻塞模式且chan未close |
- c.recvq中有等待读的接收者,将其出队,将数据直接copy给接收者,并唤醒接收者。
1 | go复制代码// 有等待的接收的goroutine |
recvq是一个双向链表,每个sudog会关联上一个reader(被阻塞的g)
当sudog出队后,会调用send方法,通过sendDirect 实现数据在两个地址之间拷贝,最后调用goready唤醒reader(被阻塞的g)
1 | go复制代码func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { |
- 缓冲区未满的情况下,数据放入环形缓冲区即可。
1 | go复制代码 // 缓冲区未满 |
- 缓冲区已满,阻塞模式下关联一个sudog数据结构并进入c.sendq队列,挂起当前协程。
1 | go复制代码 // 阻塞的情况 |
chan 读操作
当对chan进行读操作时,编译器会替换成 chanrecv1或者chanrecv2函数,最终会调用chanrecv函数处理读取
1 | go复制代码// v := <- ch |
和chansend一样,chanrecv也是支持非阻塞式的调用
1 | go复制代码func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) |
chanrecv有以下几种处理流程:
- 读nil chan,如果是非阻塞,直接返回;如果是阻塞式,将当前协程挂起。
1 | go复制代码 // 读阻塞 |
- 非阻塞模式下,没有缓冲区且没有等待写的writer或者缓冲区没数据,直接返回。
1 | go复制代码 if !block && (c.dataqsiz == 0 && c.sendq.first == nil || |
- chan已经被close,并且队列中没有数据时,会将存放值的变量清零,然后返回。
1 | go复制代码 // c已经被close 并且 没有数据 |
- sendq中有等待的writer,writer出队,并调用recv函数
1 | go复制代码// 从sendq中取出sender |
recv在这分两种处理:如果ch不带缓冲区的话,直接将writer的sg.elem数据拷贝到ep;如果带缓冲区的话,此时缓冲区肯定满了,那么就从缓冲区队列头部取出数据拷贝至ep,然后将writer的sg.elem数据拷贝到缓冲区中,最后唤醒writer(g)
1 | go复制代码func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { |
- 直接从缓冲队列中读数。
1 | go复制代码 // 带缓冲区 |
- 阻塞的情况,缓冲区没有数据,且没有writer
1 | go复制代码 |
close 关闭操作
当close一个chan时,编译器会替换成对closechan函数的调用,将closed字段置为1,并将recvq和sendq中的goroutine释放唤醒,对sendq中未写入的数据做清除,且writer会发生panic异常。
1 | go复制代码func closechan(c *hchan) { |
chan使用小技巧
- 避免read、write一个nil chan
1 | go复制代码func main() { |
- 从chan中read时,使用带指示的访问方式,读取的时候无法感知到close的关闭
1 | go复制代码func main() { |
- 从chan中read时,不要使用已存在变量接收, chan close之后,缓冲区没有数据的话,使用存在变量读取时,会将变量清零
1 | go复制代码func main() { |
- 使用select+default可以实现 chan的无阻塞读取
1 | go复制代码// 使用select反射包实现无阻塞读写 |
原因是如果select的case中存在default,对chan的读写会使用无阻塞的方法
1 | go复制代码func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) { |
本文转载自: 掘金