channel是Go在语言层面提供的协程间的通信方式。通过channel我们可以实现多个协程之间的通信,并对协程进行并发控制。
使用注意:
-
管道没有缓冲区时,从管道中读取数据会阻塞,直到有协程向管道中写入数据。类似地,向管道中写入数据会阻塞,直到有协程从管道读取数据。
-
管道有缓冲区时,从管道中读取数据,如果缓冲区没有数据也会进行阻塞,直到有协程向管道中写入数据。向管道写入数据,如果缓冲区已满也会进行阻塞,直到有协程从管道中读取数据。
-
对于一个值为nil的管道,无论读写数据都会阻塞,而且是永久阻塞。
-
读取已经关闭的管道,如果管道有缓冲区的话,会获取缓冲区中的数据,如果缓冲区中没有数据会返回零值
-
向已经关闭的管道中写入数据会触发panic【panic:send on closed channel】
-
读取管道的时候会返回两个值,第一个是从管道中取出元素的值,第二个是检测管道的缓冲区中是否还有元素,如果有返回true,没有返回false。
-
我们从一个已经关闭的管道中读取元素,如果这一个管道有缓冲区并且缓冲区中有元素,那么会获取缓冲区中的元素并true。如果这一个缓冲区没有元素,那么就会返回该管道类型的零值和false。
-
底层实现:
channel的底层数据结构实现源码位置:src/runtime/chan.go:hchan
type hchan struct {qcount uint // 当前队列中的元素个数dataqsiz uint // 队列的长度buf unsafe.Pointer // 指向一个数组空间,channel中的队列是通过数组实现的elemsize uint16 // 类型的大小closed uint32 // 是否已经关闭elemtype *_type // 管道的类型sendx uint // 元素写入队列中的下标位置recvx uint // 从环形队列中读取元素的位置recvq waitq // 待读取队列,阻塞中sendq waitq // 待写入队列,阻塞中lock mutex //锁,防止多个协程同时操作一个管
}
channel的底层实现由环形队列、类型信息、等待队列三部分组成。
-
创建管道的过程实际就是初始化hchan结构体的过程
-
向管道中写入数据时
-
如果缓冲区有空余位置,则将数据写入缓冲区,结束发送过程
-
如果缓冲区中没有位置,则将当前协程加入sendq队列,进入睡眠并等待被取协程唤醒。如果读取队列不为空,则会直接将数据写入到等待读取的协程中,并等待唤醒读取的协程。【先检查recvq是否为空,再检查buf是否有空位置】
-
-
从管道中读取数据
-
如果缓冲区有数据,则直接读取缓冲区中数据,结束读取过程
-
如果缓冲区中没有数据,则将当前协程加入recvq队列,进入睡眠并等待被写入协程唤醒。如果写入队列不为空,并且没有缓冲区,那么此时将直接从sendq中的第一个协程获取数据。【先检查sendq是否为空,之后根据判断,如果为空,检查环形队列中是否有数据;如果不为空,检查是否有缓冲区,如果有缓冲区将取出缓冲区中的第一个数据,并将sendq队列中的第一个协程中的数据写入到buf中,如果没有缓冲区,则直接读取sendq队列中的第一个协程】
-
-
关闭管道
关闭管道的时候会把recvq中的协程全部唤醒,这些协程获取到的数据都为对应类型的零值。同时也会把sendq队列中的协程全部唤醒,但是这些协程会触发panic。
常见用法:
- switch-case 随机选择一个可执行的case进行执行,如果所有的case都不能执行,则执行default,如果没有default的话,则进行阻塞等待,直到有一个可执行的case。
- for-range 可以作用于管道,如果遍历的管道没有关闭,则会将管道中的所有的元素遍历并取出,并阻塞读取管道中的元素;如果遍历的是一个已经关闭的管道,则会取出管道中的所有的元素,并正常关闭。