Goroutine 泄露 - 被遗忘的发送者

介绍

发编程允许开发人员使用一种以上的路径解决问题,并且通常用于尝试提高性能。并发编程并不意味着多个路径并行执行。这意味着这些路径是乱序执行,而不是顺序执行的。从历史上看,这种类型的编程是使用由标准库或第三方开发人员提供的库来促进的。

在 Go 中,Goroutines 和 channels 等并发特性内置于语言和运行中,以减少或消除对库的需求。这造成了在 Go 中编写并发程序很容易的错觉。 在决定使用并发时必须谨慎,因为如果使用不当,它会带来一些独特的副作用或陷阱。 如果你不小心,这些陷阱会造成复杂性和令人讨厌的错误。

我将在这篇文章中讨论的陷阱与 Goroutine 泄漏有关。

泄露 Goroutines

在内存管理方面,Go 会为你处理许多细节。 Go 编译器使用转义分析来决定值在内存中的位置。 运行时通过使用垃圾收集器来跟踪和管理堆分配。 虽然在你的应用程序中创建内存泄漏并非不可能,但几率大大降低。

一种常见的内存泄漏类型是 Goroutine 泄漏。 如果你启动了一个你希望最终终止但它从未终止的 Goroutine,那么它已经泄漏了。 它在应用程序的整个生命周期内都存在,并且为 Goroutine 分配的任何内存都无法释放。 这是 Never start a goroutine without knowing how it will stop 的建议背后的部分原因。

为了说明基本的 Goroutine 泄漏,请查看以下代码:

### 清单 1 https://play.golang.org/p/dsu3PARM24K

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
31 // leak is a buggy function. It launches a goroutine that
32 // blocks receiving from a channel. Nothing will ever be
33 // sent on that channel and the channel is never closed so
34 // that goroutine will be blocked forever.
35 func leak() {
36     ch := make(chan int)
37 
38     go func() {
39         val := <-ch
40         fmt.Println("We received a value:", val)
41     }()
42 }

注:原文写的比较啰嗦,这里简单概括下。

  1. 清单 1 定义了一个方法 leak
  2. 在36行定义了一个 channel 接收整形数据
  3. 38~41行 创建了一个 goroutine 接收来自 channel 的值。

问题:这个 goroutine 永远都接收不到值,因为逻辑里面并没有传递。 这样的方法调用后就产生了 goroutine 的泄露。

泄露:被遗忘的发送者

For this leak example you will see a Goroutine that is blocked indefinitely, waiting to send a value on a channel.

The program we will look at finds a record based on some search term which is then printed. The program is built around a function called search:

Listing 2 https://play.golang.org/p/o6_eMjxMVFv

对于这个泄漏示例,你将看到一个无限期阻塞的 Goroutine,等待在通道上发送值。

清单 4

注:清单2~3和并发没太大关系,故省略。

https://play.golang.org/p/m0DHuchgX0A

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
38 // result wraps the return values from search. It allows us
39 // to pass both values across a single channel.
40 type result struct {
41     record string
42     err    error
43 }
44 
45 // process is the work for the program. It finds a record
46 // then prints it. It fails if it takes more than 100ms.
47 func process(term string) error {
48 
49     // Create a context that will be canceled in 100ms.
50     ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
51     defer cancel()
52 
53     // Make a channel for the goroutine to report its result.
54     ch := make(chan result)
55 
56     // Launch a goroutine to find the record. Create a result
57     // from the returned values to send through the channel.
58     go func() {
59         record, err := search(term)
60         ch <- result{record, err}
61     }()
62 
63     // Block waiting to either receive from the goroutine's
64     // channel or for the context to be canceled.
65     select {
66     case <-ctx.Done():
67         return errors.New("search canceled")
68     case result := <-ch:
69         if result.err != nil {
70             return result.err
71         }
72         fmt.Println("Received:", result.record)
73         return nil
74     }
75 }

注:原文写的比较啰嗦,这里简要概括下。

通过上面的代码我们可以知道以下信息

  1. 上下面定义了超时的100ms
  2. search 执行需要200ms
  3. 所以结果是:search canceled

看上去没什么问题,但是 ch <- result{record, err} 这里永远也执行不到, ch 定义又是阻塞的。所以 search 这段 goroutine 泄露了。

修复:腾出一些空间

解决此泄漏的最简单方法是将通道从无缓冲通道更改为容量为 1 的缓冲通道。

清单 5

https://play.golang.org/p/u3xtQ48G3qK

1
2
3
53     // Make a channel for the goroutine to report its result.
54     // Give it capacity so sending doesn't block.
55     ch := make(chan result, 1)

现在在超时情况下,在接收者继续前进后,search goroutine 将通过将结果值放入 channel 来完成发送,然后返回。 该 goroutine 的内存以及 channel 的内存最终将被回收。 一切都会自然而然地解决。

结论

通过以上的错误示例,我们知道 goroutine 非常容易被用错,导致泄露。

在使用 goroutine 时,请先进行以下灵魂拷问: 它从哪里来,到那里去?

  • 什么时候终止?
  • 什么会阻止它终止?

并发是一种有用的工具,但必须谨慎使用。

本文由roy翻译自:https://www.ardanlabs.com/blog/2018/11/goroutine-leaks-the-forgotten-sender.html

updatedupdated2021-09-142021-09-14
Load Comments?