Go语言主/父goroutine退出时未完成子goroutine运行机制
Go并发示例:带缓冲channel场景下的剩余goroutine行为说明
示例代码回顾
func mirroredQuery() string{ responses := make(chan string, 3) go func() { responses <- request("asia.gopl.io") }() go func() { responses <- request("americas.gopl.io") }() go func() { responses <- request("europe.gopl.io") }() return <-responses // return the quickest response }
书中针对无缓冲channel的注释说明:
如果使用无缓冲channel,两个响应较慢的goroutine会尝试向不存在接收方的channel发送响应,最终永久阻塞。
核心问题解答
1. 慢goroutine的运行状态
只要主goroutine没有退出、程序没有整体终止,mirroredQuery返回后,两个较慢的goroutine会正常运行至结束,不会被自动取消,也不会发生阻塞泄漏。
2. responses通道的生命周期
Go中channel的生命周期和普通值一致,由可达性(是否存在活跃引用)决定,和创建它的函数是否返回没有直接关系:
- 三个启动的goroutine的闭包都持有对
responses通道的引用,因此mirroredQuery返回、函数栈帧销毁时,responses通道仍然存在活跃引用,不会被垃圾回收器立刻回收。 - 该通道缓冲区大小为3,足够容纳全部三个请求的返回值。慢goroutine执行完
request调用后,向通道发送值时不需要等待接收方,只要缓冲区有空位就会直接把值写入缓冲区、立刻返回,随后goroutine正常退出。 - 等两个慢goroutine全部执行完毕退出后,就没有任何活跃引用指向
responses通道,垃圾回收器会在后续合适的时机回收通道本身,以及缓冲区中存放的两个未被读取的返回值。
3. 和无缓冲channel场景的本质差异
书中提到的无缓冲channel泄漏问题,根源不是函数返回销毁了channel,而是无缓冲channel的发送操作必须和接收操作配对才能完成:mirroredQuery只会接收一次最快的响应就返回,后续没有任何代码会再从该channel读取数据。剩下两个慢goroutine执行到发送操作时,永远等不到接收方,就会永久陷入阻塞,形成goroutine泄漏。
而容量为3的带缓冲channel场景下,发送操作不需要等待接收方就绪,只要缓冲区未满就能直接完成,因此慢goroutine的发送操作不会卡住,自然可以正常执行完毕退出。
4. 实现的优化点
这个示例的写法不会造成goroutine泄漏,但会产生不必要的资源开销:两个慢请求会完整执行完毕,哪怕它们的返回值根本不会被使用。生产环境下这类场景通常会配合context的取消机制,在拿到最快响应后主动取消剩下两个未完成的请求,避免浪费网络、CPU等资源。
内容的提问来源于stack exchange,提问作者nos
相关产品推荐
相关产品推荐

