## 介绍
Goroutine 内存泄漏是产生 Go 程序内存泄漏的常见原因。在我之前的[文章](/articles/17364)中,我介绍了 Goroutine 内存泄漏,并展示了许多 Go 开发人员容易犯错的例子。继续前面的内容,这篇文章提出了另一个关于 Goroutines 如何出现内存泄露的情景。
## 泄漏:被遗弃的接收者
***在此内存泄漏示例中,您将看到多个 Goroutines 被阻塞等待接收永远不会发送的值。***
文章中程序启动了多个 Goroutines 来处理文件中的一批记录。每个 Goroutine 从输入通道接收值,然后通过输出通道发送新值。
### 示例一
[https://play.golang.org/p/Jtpla_UvrmN](https://play.golang.org/p/Jtpla_UvrmN)
```golang
35 // processRecords is given a slice of values such as lines
36 // from a file. The order of these values is not important
37 // so the function can start multiple workers to perform some
38 // processing on each record then feed the results back.
39 func processRecords(records []string) {
40
41 // Load all of the records into the input channel. It is
42 // buffered with just enough capacity to hold all of the
43 // records so it will not block.
44
45 total := len(records)
46 input := make(chan string, total)
47 for _, record := range records {
48 input <- record
49 }
50 // close(input) // What if we forget to close the channel?
51
52 // Start a pool of workers to process input and send
53 // results to output. Base the size of the worker pool on
54 // the number of logical CPUs available.
55
56 output := make(chan string, total)
57 workers := runtime.NumCPU()
58 for i := 0; i < workers; i++ {
59 go worker(i, input, output)
60 }
61
62 // Receive from output the expected number of times. If 10
63 // records went in then 10 will come out.
64
65 for i := 0; i < total; i++ {
66 result := <-output
67 fmt.Printf("[result ]: output %s\n", result)
68 }
69 }
70
71 // worker is the work the program wants to do concurrently.
72 // This is a blog post so all the workers do is capitalize a
73 // string but imagine they are doing something important.
74 //
75 // Each goroutine can't know how many records it will get so
76 // it must use the range keyword to receive in a loop.
77 func worker(id int, input <-chan string, output chan<- string) {
78 for v := range input {
79 fmt.Printf("[worker %d]: input %s\n", id, v)
80 output <- strings.ToUpper(v)
81 }
82 fmt.Printf("[worker %d]: shutting down\n", id)
83 }
```
在第39行,`processRecords` 定义了一个被调用的函数。该函数接受 `[]string` 值。在第46行,`input` 创建一个被调用的缓冲通道。第47和48行运行一个循环,复制 `string` 切片中的每个值并将它们发送到通道。`input` 创建的通道具有足够的容量来保存切片中的每个值,因此第48行上的通道发送都不会阻塞。此通道是用于在多个 Goroutines 之间分配值的管道。
接下来在第56到60行,该程序创建了一个 Goroutines 池来接收管道中的工作。在第56行,创建一个名为 `output` 的缓冲通道; 这是每个 Goroutine 将发送其结果的地方。第57到59行运行循环并使用 `worker` 函数创建多个 Goroutines。 Goroutines 的数量等于机器上的逻辑 CPU 的数量。循环变量的副本 `i` 以及 `input` 和 `output` 通道都传递给 Goroutine。
`worker` 函数在第77行定义。函数的签名定义中 `input` 为 `<-chan string` ,这意味着它是一个只接收通道。该函数也接受 `output` 参数, `chan<- string` 类型这意味着它是一个只发送通道。
示例第78行,在函数内部 Goroutines 使用 `range` 循环从 `input` 通道接收数据,直到通道关闭并且没有值。对于每次迭代,将接收到的值分配给 `v` 并在第79行打印迭代变量。然后在第80行,`worker` 函数传递 `v` 给 `strings.ToUpper` 函数返回新的 `string` ,并立即在 `output` 上发送新的 `string` 。
回到 `processRecords` 函数中,执行已经向下移动到第65行,在那里运行另一个循环。该循环迭代,直到它接收并处理了来自 `output` 通道的所有值。在第66行, `processRecords` 函数等待从一个工作者 Goroutines 接收一个值。接收到的值打印在第67行。当程序收到每个输入的值时,它退出循环并终止。
运行此程序打印转换后的数据,因此它似乎工作,但该程序正存在多个 Goroutines 内存泄漏。该程序从未到达第82行,该行将宣布程序正在关闭。即使在 `processRecords` 函数返回之后,每个 `worker` Goroutines 仍处于活动状态并等待第78行的输入。通道会一直接收数据直到通道关闭并为空。问题是程序永远不会关闭通道。
## 修复:信号完成
修复泄漏只需要一行代码: `close(input)` 。关闭频道是表示”不再发送数据“的一种方式。关闭通道的最合适位置是在第50行发送最后一个值之后,如示例二所示:
### 示例二
[https://play.golang.org/p/QNsxbT0eIay](https://play.golang.org/p/QNsxbT0eIay)
```golang
45 total := len(records)
46 input := make(chan string, total)
47 for _, record := range records {
48 input <- record
49 }
50 close(input)
```
关闭缓冲区中仍有值的缓冲通道是有效的; 频道仅关闭发送而不是接收。 `worker` Goroutines 运行 `range input` 将通过缓冲区来工作,直到他们发出通道已关闭的信号。这可以让 `workers` 在终止之前完成循环。
## 结论
正如前一篇文章中所提到的,Go 使得启动 Goroutines 变得简单,但是你有责任仔细使用它们。在这篇文章中,我展示了另一个很容易出现的 Goroutine 错误。还有很多方法可以创建 Goroutine 内存泄漏以及使用并发时可能遇到的其他陷阱。未来的帖子将继续讨论这些问题。与往常一样,我将继续重复这一建议:“如果不知道它会如何停止,就不要开始使用 goroutine ”。
***并发是一种有用的工具,但必须谨慎使用。***
via: https://www.ardanlabs.com/blog/2018/12/goroutine-leaks-the-abandoned-receivers.html
作者:Jacob Walker 译者:lovechuck 校对:dingdingzhou
本文由 GCTT 原创翻译,Go语言中文网 首发。也想加入译者行列,为开源做一些自己的贡献么?欢迎加入 GCTT!
翻译工作和译文发表仅用于学习和交流目的,翻译工作遵照 CC-BY-NC-SA 协议规定,如果我们的工作有侵犯到您的权益,请及时联系我们。
欢迎遵照 CC-BY-NC-SA 协议规定 转载,敬请在正文中标注并保留原文/译文链接和作者/译者等信息。
文章仅代表作者的知识和看法,如有不同观点,请楼下排队吐槽