首页 > 解决方案 > 这段带有无缓冲通道的代码会导致 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

有人可以帮忙解释一下

任何建议将不胜感激,在此先感谢!

标签: gogoroutine

解决方案


您的代码存在双重问题。

首先,理论上存在 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
    }
}

游乐场


推荐阅读