Go语言向已关闭缓冲channel写入为何结果随机?
问题代码
func writeToClosedBufferedChannel() { ch := make(chan int, 2) ch <- 10 ch <- 20 go func() { fmt.Println("test") ch <- 30 }() time.Sleep(100 * time.Millisecond) fmt.Println("before") close(ch) fmt.Println("after") for value := range ch { fmt.Println(value) } }
运行的三种随机结果
test before after panic: send on closed channel test before after 10 20 test before after 10 20 panic: send on closed channel
原因分析
三种随机结果的核心是goroutine调度的不确定性,以及Channel关闭和发送操作的竞态关系:
第一种结果:发送阻塞后遇Channel关闭触发panic
主goroutine执行time.Sleep(100ms)后,子goroutine已执行到ch <- 30,但此时Channel缓冲已满(容量2,已存10、20),子goroutine被阻塞在发送操作上,进入Channel的sendq等待队列。随后主goroutine关闭Channel,处于sendq中的发送操作会立即触发panic,主goroutine的range循环还未执行就被中断。第二种结果:发送操作未被调度,程序正常退出
主goroutine关闭Channel后,子goroutine的ch <- 30因调度延迟还未执行。等子goroutine获得调度机会时,主goroutine已经通过range循环读完缓冲里的10、20并退出程序,发送操作没机会执行,因此不会触发panic。第三种结果:range循环完成后发送触发panic
主goroutine关闭Channel后,先执行range循环读完10、20,此时子goroutine才被调度执行ch <- 30,因Channel已关闭,直接触发panic。
关于sendq的细节
当缓冲Channel已满时,发送操作会将当前goroutine放入Channel的sendq队列,等待接收端读取数据腾出空间。若此时Channel被关闭,Go运行时会遍历sendq中的所有goroutine,唤醒它们并让发送操作触发panic。但这个唤醒动作和主goroutine的range循环执行顺序完全由Go调度器决定,没有固定规则,这就是结果随机的根本原因。
内容的提问来源于stack exchange,提问作者QuickDzen

