概要
记录一下go语言中常见的goroutine泄露。在内存管理方面,Go 为您处理了许多细节。Go 编译器使用逃逸分析决定值在内存中的位置。运行时通过使用垃圾收集器来跟踪和管理堆分配。尽管在您的应用程序中产生内存泄漏并非不可能,但这种可能性会大大降低。
一种常见的内存泄漏类型是 Goroutines 泄漏。如果你启动了一个 Goroutine,你希望它最终终止,但它从未终止,那么它就已经泄漏了。它在应用程序的生命周期内存在,并且不能释放为 Goroutine 分配的任何内存。这是“永远不要在不知道 goroutine 将如何停止的情况下启动它”这一建议背后的部分原因。
代码示例
清单1
// leak 是一个有问题的函数。 它启动一个 goroutine 来阻止从通道接收。
// 该通道上永远不会发送任何内容,并且该通道永远不会关闭,因此 goroutine 将永远被阻塞。
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 定义了一个名为 的函数leak。该函数在第 36 行创建了一个通道,允许 Goroutines 传递整数数据。然后在第 38 行创建 Goroutine,它在第 39 行阻塞等待从通道接收值。当 Goroutine 正在等待时,leak函数返回。此时,程序的其他部分都不能通过通道发送信号。这使得第 39 行阻塞的 Goroutine 无限期等待。第 40 行的调用fmt.Println永远不会发生。
对于这个泄漏示例,看到了一个无限期阻塞的 Goroutine,等待在通道上发送一个值.
bug修复:增加channel接收者或者关闭channel
35 func leak() {
36 ch := make(chan int)
37 //defer close(ch) // 方法一: 当接收者就绪阻塞的时候,执行完leak(),会关闭channel,go func()会解除阻塞。
38 go func() {
39 val := <-ch
40 fmt.Println("We received a value:", val)
41 }()
42 ch <- 1 // 方法二: 补全发送者代码。
43 }
清单2
// 搜索模拟一个根据搜索词查找记录的函数。 执行这项工作需要 200 毫秒。
31 func search(term string) (string, error) {
32 time.Sleep(200 * time.Millisecond)
33 return "some value", nil
34 }
// 进程是程序的工作。 它找到一条记录然后打印它
19 func process(term string) error {
20 record, err := search(term)
21 if err != nil {
22 return err
23 }
24
25 fmt.Println("Received:", record)
26 return nil
27 }
在第 19 行的清单 3 中,定义了一个调用的函数process,它接受一个string表示搜索词的参数。在第 20 行,term变量随后被传递给search返回记录和错误的函数。如果发生错误,则在第 22 行将错误返回给调用者。如果没有错误,则在第 25 行打印记录。
对于某些应用程序,顺序调用时产生的延迟search可能是不可接受的。假设search不能使该函数运行得更快,process可以更改该函数以不消耗由 引起的总延迟成本search。
为此,可以使用 Goroutine,如下面的代码 所示。不幸的是,第一次尝试存在错误,因为它会造成潜在的 Goroutine 泄漏。
// result包装了搜索的返回值。 它允许我们通过单个通道传递这两个值
40 type result struct {
41 record string
42 err error
43 }
44
45 // 进程是程序的工作。 它找到了一条记录
46 // 然后打印出来。 如果超过 100 毫秒,它将失败。
47 func process(term string) error {
48
49 // 创建一个将在 100 毫秒内取消的上下文。
50 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
51 defer cancel()
52
53 // 为 goroutine 创建一个通道来报告它的结果。
54 ch := make(chan result)
55
56 // 启动一个 goroutine 来查找记录。 创建结果
57 // 从返回值通过通道发送。
58 go func() {
59 record, err := search(term)
60 ch <- result{record, err}
61 }()
62
63 // 阻塞等待从 goroutine 接收
64 // 通道或要取消的上下文。
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 }
这段代码的问题在于当search()函数需要执行200ms,第50行我们设置的context timeout时间为100ms,通过select代码块先接收到ctx.Done()的timeout返回,<- ch不会被执行,go func() {}将一直阻塞。此时就是goroutine阻塞。
bug修复:解决此泄漏的最简单方法是将通道从无缓冲通道更改为容量为 1 的缓冲通道。
47 func process(term string) error {
48
49 // 创建一个将在 100 毫秒内取消的上下文。
50 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
51 defer cancel()
52
53 // 为 goroutine 创建一个通道来报告它的结果。fix bug: 将无缓冲channel改为缓冲为1的channel.
54 ch := make(chan result,1)
55
56 // 启动一个 goroutine 来查找记录。 创建结果
57 // 从返回值通过通道发送。
58 go func() {
59 record, err := search(term)
60 ch <- result{record, err}
61 }()
62
63 // 阻塞等待从 goroutine 接收
64 // 通道或要取消的上下文。
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 }
测试goroutine泄露
// 测试case
func TestCase02(t *testing.T) {
// 获取开始的goroutine数量
startingGs := runtime.NumGoroutine()
if err := process("gophers"); err != nil {
log.Print(err)
}
// 短暂停止,让goroutine得以执行
time.Sleep(time.Second)
// goroutine运行结束后再次获取当前goroutine
endingGs := runtime.NumGoroutine()
// 输出结果
fmt.Println("========================================")
fmt.Println("Number of goroutines before:", startingGs)
fmt.Println("Number of goroutines after :", endingGs)
fmt.Println("Number of goroutines leaked:", endingGs-startingGs)
}
清单2goroutine泄露的测试
=== RUN TestCase02
2023/05/24 17:51:03 search canceled
========================================
Number of goroutines before: 2
Number of goroutines after : 3
Number of goroutines leaked: 1 // 存在一个goroutine泄露
--- PASS: TestCase02 (1.10s)
PASS
清单2goroutine泄露修复的测试
=== RUN TestCase02
2023/05/24 17:51:27 search canceled
========================================
Number of goroutines before: 2
Number of goroutines after : 2
Number of goroutines leaked: 0
--- PASS: TestCase02 (1.10s)
PASS
小结
Go 使启动 Goroutines 变得简单,但我们有责任明智地使用它们。在使用并发时, 任何时候启动一个 Goroutine 时,你都必须问自己:
什么时候终止?
什么可以阻止它终止?