这段具有非缓冲通道的代码是否会在Go中导致goroutine泄漏?

时间:2019-05-28 19:42:21

标签: go goroutine

我正在使用goroutine和通道编写一些golang并发代码,怀疑我的代码可能会导致goroutine泄漏。我的情况类似于以下代码,或者您可以打开此go playground link

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泄漏。这篇文章goroutine leak with buffered channel in Go也暗示了So only if the channel was unbuffered the leak would occur

Go编程语言建议:

  

在测试过程中,我们可以使用一个方便的技巧:如果在取消事件发生时而不是从main返回时,我们执行了panic调用,那么运行时将转储程序中每个goroutine的堆栈。如果主goroutine仅剩一个,则说明它已经清除了。但是,如果仍然存在其他goroutine,则它们可能尚未正确取消,或者可能已被取消,但取消需要时间;进行一些调查可能是值得的。恐慌转储通常包含足够的信息以区分这些情况。

因此,我在主要功能的末尾添加了panic("Goroutine Resource Leak Test")以验证我的假设。但是,紧急转储仅包含主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泄漏,或者
  • 如果有泄漏,我应该如何获得正确的紧急转储

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

1 个答案:

答案 0 :(得分:4)

您的代码存在双重问题。

首先,从理论上讲,有一个 泄漏,因为任何将值发送到容量为零的通道(未缓冲的通道或已满的缓冲通道)的尝试都会阻止发送goroutine,直到出现接收操作已在该通道上完成。

是的,是的,通过定义通道的工作方式,您的所有三个goroutine将在numChan <- num语句中被阻止。

第二,由于对Go进行了某些修订,因此默认情况下未处理的panic仅转储恐慌goroutine的堆栈跟踪。 如果您希望转储所有活动goroutine的堆栈,则必须调整运行时-从documentation of the package runtime

  

GOTRACEBACK变量控制在Go程序由于无法恢复的紧急情况或意外的运行状况而失败时生成的输出量。默认情况下,故障将打印当前goroutine的堆栈跟踪,清除运行时系统内部的函数,然后以退出代码2退出。如果没有当前goroutine或故障是失败,则故障将打印所有goroutine的堆栈跟踪。运行时内部的。 GOTRACEBACK=none完全省略了goroutine堆栈跟踪。 GOTRACEBACK=single(默认)的行为如上所述。 GOTRACEBACK=all为所有用户创建的goroutine添加堆栈跟踪。 GOTRACEBACK=system类似于“全部”,但为运行时函数添加了堆栈框架,并显示了运行时在内部创建的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在调用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
    }
}

Playground