【golang】channel原理和机制
本文结合源码及channel使用场景对其原理进行解析
一、channel数据结构:
type hchan struct {qcount uint // total data in the queuedataqsiz uint // size of the circular queuebuf unsafe.Pointer // points to an array of dataqsiz elementselemsize uint16closed uint32elemtype *_type // element typesendx uint // send indexrecvx uint // receive indexrecvq waitq // list of recv waiterssendq waitq // list of send waiterslock mutex
}
核心结构
1.环形数组,承载channel的缓冲功能。环形数组本质上是由 定长数组 + 头尾下标组成,之所以叫做环形数组,在于尾部指针触尾后可以移动到头部,剩余空间可以得到充分利用,同时数组具有天然的局部性原理
2.锁,channel支持并发操作,必然要对其buffer做线程安全的设计
3.读队列和写队列,两个等待队列,如果有操作该channel的协程因锁被占用而阻塞,将陷入阻塞的协程使用等待队列管理,便于后续唤醒。插个眼,这个特性将作为后续的使用场景解析的前置条件
二、有缓冲和无缓冲channel
有无缓冲是根据hchan对象的buf是否为空决定,若为无缓冲型,则仅申请一个大小为默认值 96 的空间
1.无缓冲
- 发送操作会阻塞至另一个协程接受数据为止
- 强同步,类似于信号量的功能
2.有缓冲
- 发送操作只会在缓冲区满时阻塞
- 弱同步,异步通信
三、初始化、未初始化channel
1.直接创建channel,未初始化,对应chan为nil
2.通过make创建会自动初始化
使用场景:
- 阻塞模式下(非select),读/写一个未初始化的chan,会死锁,该协程不会被唤醒;非阻塞模式下返回error
- 对于已经关闭的chan,写操作会painc
- 对于已经关闭的chan,如果缓冲区有空间,正常读取并返回数据和true,如果缓冲区为空,返回零值(0或“”)和fasle
四、写操作场景(需加锁)
1.写操作时存在阻塞的读协程
读阻塞意味着写入前缓冲区为空,此时写协程会直接从阻塞队列里取出一个协程,直接memmove数据到对方,并唤醒该协程
2.写时无阻塞读协程且环形缓冲区无空间
此时会单纯因为缓冲区满而阻塞该写协程,将其加入到写阻塞队列中,主动park阻塞。直到有读协程因缓冲区空而去唤醒该阻塞的写协程
五、读操作场景(需加锁)
1.读时有阻塞的写协程
说明缓冲区满,写操作阻塞
如果channel无缓冲区,直接唤醒该写协程,读取其元素,也就是强同步的逻辑
如果channel有缓冲区,先读缓冲区,然后再唤醒一个写阻塞的协程(因为读后缓冲区有空间了)
2.读时无阻塞写协程且缓冲区无元素
也就是目前无法读到数据的情况,将该协程加入读阻塞队列,主动park陷入阻塞
六、阻塞与非阻塞模式
结论:除select外,默认channel 为阻塞模式
1.非阻塞模式逻辑区别
在select非阻塞模式下,读/写 channel 方法通过一个 bool 型的响应参数,用以标识是否读取/写入成功,实际并不阻塞,而是cas轮询
• 所有需要使得当前 goroutine 被挂起的操作,在非阻塞模式下都会返回 false;
• 所有是的当前 goroutine 会进入死锁的操作,在非阻塞模式下都会返回 false;
• 所有能立即完成读取/写入操作的条件下,非阻塞模式下会返回 true.
为什么select需要非阻塞?
select的初衷就是多路复用,同时监听多个事件,如果某一个分支因为:
1.读/写一个未初始化的chan,会死锁
2.读/写操作阻塞,导致整个select协程阻塞
所以非阻塞模式下,若某一个case未就绪,会直接返回并持续轮询
七、关闭channel
- 关闭未初始化过的 channel 会 panic;
- 重复关闭 channel 会 panic;
- 唤醒 该channel的阻塞队列里的所有协程