go - 这段带有无缓冲通道的代码会导致 Go 中的 goroutine 泄漏吗?
问题描述
我正在用goroutines和channels编写一些golang并发代码,怀疑我的代码可能会导致goroutine泄漏。我的情况类似于下面的代码,或者你可以打开这个go playground 链接。
func main() {
numCount := 3
numChan := make(chan int)
for i := 0; i < numCount; i++ {
go func(num int) {
fmt.Printf("Adding num: %d to chan\n", num)
numChan <- num
fmt.Printf("Adding num: %d to chan Done\n", num)
}(i)
}
time.Sleep(time.Second)
panic("Goroutine Resource Leak Test")
}
在我看来,当主 goroutine 返回时,三个 goroutine 被阻塞发送到无缓冲通道,并且会出现 goroutine 泄漏。这个 post goroutine leak with buffered channel in Go也表明So only if the channel was unbuffered the leak would occur
.
Go 编程语言建议:
我们可以在测试期间使用一个方便的技巧:如果在取消的情况下不从 main 返回,而是执行对 panic 的调用,那么运行时将转储程序中每个 goroutine 的堆栈。如果主 goroutine 是唯一剩下的,那么它已经自行清理了。但是如果还有其他的 goroutines,它们可能没有被正确地取消,或者它们可能已经被取消但取消需要时间;一点调查可能是值得的。恐慌转储通常包含足够的信息来区分这些情况。
因此,我添加panic("Goroutine Resource Leak Test")
到 main 函数的末尾来验证我的假设。但是,panic dump 只包含 main goroutine,即没有资源泄漏。
Adding num: 0 to chan
Adding num: 1 to chan
Adding num: 2 to chan
panic: Goroutine Resource Leak Test
goroutine 1 [running]:
main.main()
/tmp/sandbox011109649/prog.go:21 +0xc0
有人可以帮忙解释一下
- 为什么没有 goroutine 泄漏,或者
- 如果有泄漏,我应该如何获得正确的恐慌转储
任何建议将不胜感激,在此先感谢!
解决方案
您的代码存在双重问题。
首先,理论上存在 goroutine 泄漏,因为任何向容量为零的通道(无缓冲通道或已填充的缓冲通道)发送值的尝试都会阻塞发送 goroutine,直到在该通道上完成接收操作。
所以,是的,根据通道如何工作的定义,你所有的三个 goroutine 都将在numChan <- num
语句中被阻塞。
其次,由于 Go 的某些版本,panic
默认情况下未处理的仅转储恐慌 goroutine 的堆栈跟踪。如果您希望转储所有活动 goroutine 的堆栈,则必须调整运行时 - 从包的文档中runtime
:
该
GOTRACEBACK
变量控制当 Go 程序由于未恢复的恐慌或意外的运行时条件而失败时生成的输出量。默认情况下,失败打印当前 goroutine 的堆栈跟踪,省略运行时系统内部的函数,然后以退出代码 2 退出。如果没有当前 goroutine 或失败是失败,则失败打印所有 goroutine 的堆栈跟踪运行时的内部。GOTRACEBACK=none
完全省略 goroutine 堆栈跟踪。GOTRACEBACK=single
(默认值)的行为如上所述。GOTRACEBACK=all
为所有用户创建的 goroutine 添加堆栈跟踪。GOTRACEBACK=system
类似于“all”,但为运行时函数添加了堆栈帧,并显示了运行时内部创建的 goroutine。GOTRACEBACK=crash
类似于“系统”,但以特定于操作系统的方式崩溃而不是退出。例如,在 Unix 系统上,崩溃引发SIGABRT
以触发核心转储。由于历史原因,GOTRACEBACK
设置 0、1 和 2 分别是 none、all 和 system 的同义词。The runtime/debug
包的SetTraceback
功能允许在运行时增加输出量,但它不能减少低于环境变量指定的量。请参阅https://golang.org/pkg/runtime/debug/#SetTraceback。
另请注意,您决不能使用计时器进行(模拟)同步:在一个玩具示例中,这可能有效,但在现实生活中,没有什么能阻止您的三个 goroutine 在您的主 goroutine 在调用中花费的时间跨度内没有机会运行to——time.Sleep
所以结果可能是运行了任意数量的衍生 goroutine:从 0 到 3。
再加上这样一个事实,即当main
退出运行时只会杀死所有未完成的活动 goroutine,并且测试的结果可能充其量是令人惊讶的。
因此,一个适当的解决方案是
- 只需在需要的地方打印堆栈,
- 确保通过匹配的接收同步发送:
package main
import (
"fmt"
"log"
"runtime"
)
func dumpStacks() {
buf := make([]byte, 32 * 1024)
n := runtime.Stack(buf, true)
log.Println(string(buf[:n]))
}
func main() {
numCount := 3
numChan := make(chan int, numCount)
for i := 0; i < numCount; i++ {
go func(num int) {
fmt.Printf("Adding num: %d to chan\n", num)
numChan <- num
fmt.Printf("Adding num: %d to chan Done\n", num)
}(i)
}
dumpStacks()
for i := 0; i < numCount; i++ {
<-numChan
}
}
游乐场。
推荐阅读
- powershell - Visual Studio Code 在非默认终端上打开并运行脚本
- flutter - 需要在 Flutter App 中检查输入的值是否为数量
- mysql - 实体框架mysql连接字符串错误?
- ansible - Ansible - 循环对象列表内的嵌套数组?
- python - syft 安装失败
- android - Jetpack Compose 在打开时将 ModalDrawer 上的状态栏颜色设置为透明
- r - 具有已知起点和终点的最陡下降路径
- typescript - 在 typescript 中创建一个 Atom 包
- python - 递归函数迭代次数超出预期
- reactjs - useState 是否应该依赖 React 中的初始 useState?