概述
阅读 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 休眠,从而发生死锁崩溃。
//在 src/runtime/chan.go中
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == nil {
// 不能阻塞,直接返回 false,表示未发送成功
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 省略其他逻辑
}
- 未初始化的
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
具体看源码。
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
当 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 休眠,从而发生死锁崩溃。
//在 src/runtime/chan.go中
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
//省略逻辑...
if c == nil {
if !block {
return
}
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
//省略逻辑...
}
- 未初始化的
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
c.closed != 0 && c.qcount == 0
指通道已经关闭,且缓存为空的情况下(已经读完了之前写到通道里的值)- 如果接收值的地址
ep
不为空- 那接收值将获得是一个该类型的零值
typedmemclr
会根据类型清理相应地址的内存- 这就解释了上面代码为什么关闭的
chan
会返回对应类型的零值
循环select读取已关闭的chan
即使chan 已经关闭,还是可以读取到零值,能读取就说明能进入select分支,一直循环。
解决:通过 v, ok := <-ch
,ok判断,如果ok为false,让ch=nil
func main() {
ch := make(chan int)
go func() {
time.Sleep(1 * time.Second)
ch <- 1
close(ch)
}()
for {
select {
case v, ok := <-ch:
time.Sleep(time.Second)
fmt.Println(v, ok)
if !ok {
ch = nil
}
// 缺少default 会 死锁(读写nil的chan)
default:
time.Sleep(time.Second)
fmt.Println("enter default")
}
}
}
无缓冲的chan接收数据
接收数据同样包含直接往接收方的执行栈中拷贝要发送的数据,但这种情况当且仅当缓存大小为0时(即无缓冲 Channel)。
关闭chan
关闭nil的channel会发生什么
会发生panic:close of nil channel
重复关闭channel会发生什么
会发生panic:close of closed channel