golang中的channel及踩坑
通道channel是golang的高并发法宝之一,作为一个go程序员,如果不会使用channel做并发操作实在是有点说不过去。
通道的结构
数据结构
1 | type hchan struct { |
读写通道的过程
发送流程:
向一个channel中写数据简单过程如下:
- 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程;
- 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
- 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被读goroutine唤醒;
简单流程图如下:
接收流程:
从一个channel读数据简单过程如下:
- 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,把G中数据读出,最后把G唤醒,结束读取过程;
- 如果等待发送队列sendq不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把G中数据写入缓冲区尾部,把G唤醒,结束读取过程;
- 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
- 将当前goroutine加入recvq,进入睡眠,等待被写goroutine唤醒;
简单流程图如下:
关闭channel
关闭channel时会把recvq中的G全部唤醒,本该写入G的数据位置为nil。把sendq中的G全部唤醒,但这些G会panic。
除此之外,panic出现的常见场景还有:
- 关闭值为nil的channel
- 关闭已经被关闭的channel
- 向已经关闭的channel写数据
无缓冲通道和缓冲为1的通道
在无缓冲通道中,读和写是相互依赖的,任意一个读或写操作没有交付,都会一直阻塞。
而缓冲为1的通道不同,以往channel发送数据为例,数据会优先交付给接收队列中等待的goroutine,如果等待队列为空,就会将数据到缓冲区;如果缓冲区满的话,才会阻塞到发送队列。
相比之下,无缓冲通道有更强的一致性,而有缓冲通道则更灵活。
在下面这个例子中,可以更好理解二者的区别:
仅使用一个管道,交替打印1-100这100个数字。
1 | func main() { |
使用缓冲区为1的管道可以正常打印:
goroutine1: 1
goroutine2: 2
……
goroutine1: 99
goroutine2: 100
但是如果将chanel更改为无缓冲管道。结果将是先正常打印数字,然后死锁:
…
goroutine2: 98
goroutine1: 99
goroutine2: 100
fatal error: all goroutines are asleep - deadlock!goroutine 1 [semacquire]:
sync.runtime_Semacquire(0x14000002101?)
/usr/local/go/src/runtime/sema.go:62 +0x2c…
原因是,无论那个goroutine先结束,另一个goroutine都会阻塞在 cur <- (id % 2) + 1
写管道这里。
例如,当 goroutine 1 打印 99 之后,发送信号给 goroutine 2,希望它打印 100。此时 goroutine 2 打印 100 后,会再次尝试发送信号给 goroutine 1。
但是,goroutine 1 已经没有数字需要打印了,因此它不会再从通道中读取信号,也不会继续工作。
goroutine 2 此时被阻塞在发送信号的操作上,等待 goroutine 1 接收,但 goroutine 1 已经结束等待,程序进入死锁状态。
而有缓冲通道则没有这个问题,goroutine直接将数据放入缓冲区buf中即可。