Channel源码解析
Contents
什么是channel
channel(管道),go的一种特殊的数据类型,像是通道(队列),先进先出,可以通过它们发送类型化的数据在协程之间通信,可以避开所有内存共享导致的坑;通道的通信方式保证了同步性。
channel 数据结构
|
|
- qcount: 当前队列/channel中剩余元素个数
- dataqsiz: 环形队列长度,即可以存放的元素个数
- buf: 环形队列指针
- elemsize: 每个元素的大小
- closed: 标识关闭状态
- elemtype: chan中元素类型
- sendx: 队列下标,指示元素写入时存放到队列中的位置
- recvx: 队列下标,指示元素从队列的该位置读出
- recvq: 等待读消息的goroutine队列
- sendq: 等待写消息的goroutine队列
- lock: 互斥锁(标准库sync/mutex)
一个channel只能传递一种类型的值,类型信息存储在hchan数据结构中。
elemtype
代表类型,用于数据传递过程中的赋值;elemsize
代表类型大小,用于在buf
中定位元素位置。
|
|
|
|
从上面的信息可以看出channel是由队列,类型信息、goroutine等待队列组成。
环形队列:
- dataqsiz指示了队列长度为6,即可缓存6个元素;
- buf指向队列的内存,队列中还剩余两个元素;
- qcount表示队列中还有两个元素;
- sendx指示后续写入的数据存储的位置,取值[0, 6);
- recvx指示从该位置读取数据, 取值[0, 6);
channel相关操作
写操作
- 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从
recvq
取出G,并把数据写入,最后把该G唤醒,结束发送过程; - 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
- 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入
sendq
,进入睡眠,等待被读goroutine
唤醒;
读操作
- 如果等待发送队列
sendq
不为空,且没有缓冲区,直接从sendq
中取出G,把G中数据读出,最后把G唤醒,结束读取过程; - 如果等待发送队列
sendq
不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把G中数据写入缓冲区尾部,把G唤醒,结束读取过程; - 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
- 将当前
goroutine
加入recvq
,进入睡眠,等待被写goroutine
唤醒;
关闭
|
|
关闭channel时会把recvq中的G全部唤醒,本该写入G的数据位置为nil。把sendq中的G全部唤醒,但这些G会panic。除此之外,panic出现的常见场景还有:
- 关闭值为nil的channel
- 关闭已经被关闭的channel
- 向已经关闭的channel写数据
优雅关闭
不要从接收端关闭通道,如果通道有多个并发发送方,也不要关闭通道。
|
|
注意事项
- 发生 panic 的情况有三种:向一个关闭的
channel
进行写操作;关闭一个nil
的channel
;重复关闭一个channel
。 - 读、写一个
nil channel
都会被阻塞 channel
的泄漏:groutine
操作channel
后,一直处于发送或接收阻塞状态,而channel
处于满或空的状态,一直得不到改变。同时,垃圾回收器也不会回收此类资源,进而导致gouroutine
会一直处于等待队列中
reference
《go专家编程》