为什么Go中channel阻塞但程序不退出?signal.NotifyContext实现差异
问题描述
我有以下两个使用signal.NotifyContext实现基于信号的context取消的版本:
版本1
func main() { ch := run() <-ch } func run() chan bool { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) var done = make(chan bool) //go func() { select { case <-ctx.Done(): fmt.Println("Quitting") stop() done <- true } //}() return done }
版本2
func main() { ch := run() <-ch } func run() chan bool { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) var done = make(chan bool) go func() { select { case <-ctx.Done(): fmt.Println("Quitting") stop() done <- true } }() return done }
问题:为什么第一个版本会打印
Quitting行但不会退出,而第二个版本打印后可以正常退出?
问题解答
两个版本的核心差异在于处理ctx.Done()监听的逻辑是否独立到子协程运行,具体原因如下:
- 版本1卡死的原因
run函数中的select直接运行在主调用协程上,没有开启新协程,因此run函数执行到select时会直接阻塞,等待ctx.Done()触发,根本不会执行到后续的return done语句。
当收到信号触发ctx.Done()后,进入case分支打印Quitting,随后执行done <- true写通道操作:此时done是无缓冲通道,而且main函数还停在ch := run()这一步等待run返回,没有任何协程在读取done通道,因此写操作会永久阻塞,程序无法继续执行。 - 版本2正常退出的原因
监听ctx.Done()的逻辑被包在独立的子协程中,run函数不会被阻塞,会直接将done通道返回给main函数,main函数紧接着执行<-ch进入读通道阻塞状态。
当收到信号触发ctx.Done()后,子协程进入case分支打印Quitting,随后执行done <- true写通道操作:此时main协程正好在等待读done通道,无缓冲通道的读写操作可以直接配对完成,main函数拿到值后正常退出,整个程序运行结束。
内容的提问来源于stack exchange,提问作者SuriG
相关产品推荐
相关产品推荐

