概述

阅读 channel 的源码,可以发现 channel 的数据结构是 hchan 结构体,包含以下字段:

  • qcount 当前队列中剩余的元素个数
  • datasize 环形队列的长度
  • buf 环形队列的指针
  • elemsize 元素的大小
  • closed 关闭标识
  • elemtype 元素的类型
  • sendx 发送索引位置
  • recvx 接收索引位置
  • recvq 等待接收的协程队列
  • sendq 等待发送的协程队列
  • lock 互斥锁

通过阅读 channel 的数据结构,可以发现 channel 是使用环形队列作为 channel 的缓冲区,datasize 环形队列的长度是在创建 channel 时指定的,通过 sendx 和 recvx 两个字段分别表示环形队列的队尾和队首,其中,sendx 表示数据写入的位置,recvx 表示数据读取的位置。

字段 recvq 和 sendq 分别表示等待接收的协程队列和等待发送的协程队列,当 channel 缓冲区为空或无缓冲区时,当前协程会被阻塞,分别加入到 recvq 和 sendq 协程队列中,等待其它协程操作 channel 时被唤醒。其中,读阻塞的协程被写协程唤醒,写阻塞的协程被读协程唤醒。

字段 elemtype 和 elemsize 表示 channel 中元素的类型和大小,需要注意的是,一个 channel 只能传递一种类型的值,如果需要传递任意类型的数据,可以使用 interface{} 类型。

字段 lock 是保证同一时间只有一个协程读写 channel。

执行逻辑

写操作 channel,分为两种情况,第一种是 channel 的缓冲区未写满,直接将数据写入缓冲区,结束 send 操作;第二种是 channel 的缓冲区已写满,此时,当前操作 channel 的协程将会被加入 sendq 等待发送的协程队列,等待被读协程唤醒。
需要注意的是,当 recvq 队列不为空时,证明缓冲区没有数据,但是有协程等待读取数据,此时,数据将不再写入缓冲区,而是会直接把数据传递给 recvq 队列中的第一个协程。

读操作 channel,也分为两种情况,第一种是 channel 的缓冲区中有数据,直接读取缓冲区中的数据,结束 recv 操作;第二种是 channel 的缓冲区中没有数据,此时,当前操作 channel 的协程加入 recvq 等待接收的协程队列,等待被写协程唤醒。
需要注意的是,当 sendq 队列不为空时,并且缓冲区已写满,此时,将直接从 sendq 队列中的第一个协程读取数据。
当 channel 被关闭时,recvq 和 sendq 中的所有协程被唤醒,其中 recvq 中的协程读取到的数据全部是 elemtype 类型零值,sendq 中的协程会触发 panic。

总结
本文我们在 channel 的数据结构和执行逻辑两个方面介绍了 channel 的实现原理,其中,执行逻辑小节中,重点介绍了 channel 的读写操作。如果读者朋友们想要了解创建 channel 的执行逻辑,可以阅读源码中的函数 func makechan(t chantype, size int) hchan。

发送数据

向nil的channel发送数据会怎样

会发生
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send (nil chan)]

原因:当向 nil channel 发送数据时,会调用 gopark。而 gopark 会将当前的 Goroutine 休眠,从而发生死锁崩溃。

参考

  1. //在 src/runtime/chan.go中
  2. func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
  3. if c == nil {
  4. // 不能阻塞,直接返回 false,表示未发送成功
  5. if !block {
  6. return false
  7. }
  8. gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
  9. throw("unreachable")
  10. }
  11. // 省略其他逻辑
  12. }
  • 未初始化的 chan 此时是等于 nil,当它不能阻塞的情况下,直接返回 false,表示写 chan 失败
  • chan 能阻塞的情况下,则直接阻塞 gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2), 然后调用 throw(s string) 抛出错误,其中 waitReasonChanSendNilChan 就是刚刚提到的报错 "chan send (nil chan)"

向关闭的且非nil的channel发送数据

会发生panic:panic: send on closed channel
具体看源码。

  1. if c.closed != 0 {
  2. unlock(&c.lock)
  3. panic(plainError("send on closed channel"))
  4. }

c.closed != 0 则为通道关闭,此时执行写,源码提示直接 panic,输出的内容就是上面提到的 "send on closed channel"

无缓冲的chan发送数据

发送数据直接往接收方的执行栈中拷贝要发送的数据,但这种情况当且仅当缓存大小为0时(即无缓冲 Channel)。


接收数据

从nil的channel接收数据

会发生
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]
原因:和send一样,向nil channel发送数据的收,会调用gopark。而 gopark 会将当前的 Goroutine 休眠,从而发生死锁崩溃。

  1. //在 src/runtime/chan.go中
  2. func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
  3. //省略逻辑...
  4. if c == nil {
  5. if !block {
  6. return
  7. }
  8. gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
  9. throw("unreachable")
  10. }
  11. //省略逻辑...
  12. }
  • 未初始化的 chan 此时是等于 nil,当它不能阻塞的情况下,直接返回 false,表示读 chan 失败
  • chan 能阻塞的情况下,则直接阻塞 gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2), 然后调用 throw(s string) 抛出错误,其中 waitReasonChanReceiveNilChan 就是刚刚提到的报错 "chan receive (nil chan)"

从关闭的非nil的channel接收数据

不管是有缓冲,还是无缓冲的channel,如果有数据,读出数据;如果没有数据,会接收到chan类型零值。可以使用 ok-idiom 方式。val, ok := <-ch
1.png

  • c.closed != 0 && c.qcount == 0 指通道已经关闭,且缓存为空的情况下(已经读完了之前写到通道里的值)
  • 如果接收值的地址 ep 不为空
    • 那接收值将获得是一个该类型的零值
    • typedmemclr根据类型清理相应地址的内存
    • 这就解释了上面代码为什么关闭的 chan 会返回对应类型的零值

循环select读取已关闭的chan

即使chan 已经关闭,还是可以读取到零值,能读取就说明能进入select分支,一直循环。
解决:通过 v, ok := <-ch,ok判断,如果ok为false,让ch=nil

  1. func main() {
  2. ch := make(chan int)
  3. go func() {
  4. time.Sleep(1 * time.Second)
  5. ch <- 1
  6. close(ch)
  7. }()
  8. for {
  9. select {
  10. case v, ok := <-ch:
  11. time.Sleep(time.Second)
  12. fmt.Println(v, ok)
  13. if !ok {
  14. ch = nil
  15. }
  16. // 缺少default 会 死锁(读写nil的chan)
  17. default:
  18. time.Sleep(time.Second)
  19. fmt.Println("enter default")
  20. }
  21. }
  22. }

参考:https://mp.weixin.qq.com/s/lK6I353Iw08robqpmPB6-g

无缓冲的chan接收数据

接收数据同样包含直接往接收方的执行栈中拷贝要发送的数据,但这种情况当且仅当缓存大小为0时(即无缓冲 Channel)。


关闭chan

关闭nil的channel会发生什么

会发生panic:close of nil channel

重复关闭channel会发生什么

会发生panic:close of closed channel