读取元素数量未知的两个Go通道时出现死锁问题排查
问题
尝试生成数字发送至通道,由另一个函数读取处理后,根据是否出错返回out和errorCh两个通道。由于错误数量未知,通过defer关闭out和errorCh以标识无更多元素。
示例中,将数字发送至in通道,再将该通道传入run函数获取out和errorCh,最后从这两个通道读取结果。通过defer关闭in通道,预期run函数收到in通道关闭信号后处理完所有元素,进而关闭out和errorCh,使select语句感知到通道关闭,但实际出现了死锁。请问是否存在通道未正确关闭的情况?死锁原因是什么?
原代码:
package main import ( "errors" "fmt" ) func run(in <-chan int) (chan int, chan error) { out := make(chan int) errorCh := make(chan error) go func() { defer close(out) defer close(errorCh) for i := range in { if i%2 == 0 { out <- i } else { errorCh <- errors.New("we don't like odd numbers") } } }() return out, errorCh } func main() { in := make(chan int) out := make(chan int) errors := make(chan error) // 生成数字发送至in通道 go func(in chan int) { defer close(in) for i := 0; i < 10; i++ { fmt.Println(" input", i) in <- i } }(in) // run函数读取in通道并处理,返回out和errorCh通道 go func(in chan int) { out, errors = run(in) }(in) // 持续读取out和errorCh通道,直至in通道所有数字处理完成 for { select { case i, ok := <-out: if !ok { // out通道已关闭 return // 处理完成 } fmt.Println("done", i) case err, ok := <-errors: if !ok { return } if err != nil { fmt.Println(err) } } } }
死锁原因分析
- 通道引用错误:main函数中先初始化了两个空的无缓冲通道
out和errors,随后在一个独立goroutine中调用run(in)重新给这两个变量赋值。但main的select语句从一开始就监听的是最初创建的空通道,完全没有使用run函数返回的、实际会发送数据并关闭的通道。这导致run内部的goroutine往返回通道发送数据时没有接收方,而main的select一直阻塞在无数据、也不会被关闭的初始通道上,最终所有goroutine都陷入阻塞,触发死锁。 - 同步缺失:赋值
out和errors的goroutine与main的select之间没有同步机制,main极有可能在goroutine完成赋值前就开始监听无效通道,彻底偏离预期逻辑。
修复方案
- 移除main中初始创建的无效通道,直接在main goroutine中调用
run获取正确的通道引用 - 修改退出逻辑:不能仅因一个通道关闭就直接返回,需等待两个通道都关闭后再退出,避免遗漏未处理的数据
修复后的代码:
package main import ( "errors" "fmt" ) func run(in <-chan int) (chan int, chan error) { out := make(chan int) errorCh := make(chan error) go func() { defer close(out) defer close(errorCh) for i := range in { if i%2 == 0 { out <- i } else { errorCh <- errors.New("we don't like odd numbers") } } }() return out, errorCh } func main() { in := make(chan int) // 生成数字发送至in通道 go func(in chan int) { defer close(in) for i := 0; i < 10; i++ { fmt.Println(" input", i) in <- i } }(in) // 直接获取run返回的有效通道 out, errorCh := run(in) outOpen := true errOpen := true // 等待两个通道都关闭后再退出 for outOpen || errOpen { select { case i, ok := <-out: if !ok { outOpen = false continue } fmt.Println("done", i) case err, ok := <-errorCh: if !ok { errOpen = false continue } if err != nil { fmt.Println(err) } } } }
内容的提问来源于stack exchange,提问作者Jonathan Kittell
相关产品推荐
相关产品推荐

