Goroutine泄露

概要

记录一下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 时,你都必须问自己:

什么时候终止?
什么可以阻止它终止?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值