防止Goroutine泄露

防止 Goroutine 泄露

goroutine 是非常廉价且容易创建的,运行时将多个 goroutine 复用到任意数量的操作系统线程,但是 goroutine 还是需要消耗资源,而且 goroutine 不会被运行时垃圾回收,所以无论 goroutine 所占用的内存有多么的少,都不希望进程对此没有感知,如何确保 goroutine 被清理干净?goroutine 有一下几种方式终止:

  • 当它完成了工作
  • 因为不可恢复的错误,它不能继续工作
  • 当它被告知需要终止工作

简单的 goroutine 泄露来了解什么事 goroutine 泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dowork :=func(strings <-chan string)<-chan interface{}{
completed :=make(chan interface{})
go func(){
defer fmt.Println("dowork exited")
defer close(completed)
for s :=range strings{
fmt.Println(s)
}
}()
return completed
}

dowork(nil)
fmt.Println("Done")

maingoroutine 将一个空的 channel 传递给 dowork,所以字符串 channel 永远不会写入任何字符串。并且包含 dowork 的 goroutine 将在此过程的整个生命周期中保留在内存中(如果我们在 dowork 和 maingoroutine 中加入了 goroutine,甚至会死锁)

在这个例子中,这个过程的生命周期很短,但是在一个真正的程序中,goroutine 可以很容易的在一个长期声明的程序开始时活动,在最糟糕的情况下,maingoroutine 可能会在其声明周期内持续的将其他的 goroutine 设置自旋,这会导致内存的利用率下降。

成功减轻这种情况的方法是在父 goroutine 和其子 goroutine 之间建立一个信号,让父 goroutine 向其子 goroutine 发出信号通知,按照惯例,这个信号通常是一个名为 done 的只读 channel,父 goroutine 将该 channel 传递给子 goroutine,然后想要取消子 goroutine 时关闭 channel。

example:

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
dowork := func(done <-chan interface{}, strings <-chan string) <-chan interface{} { // 1
terminated := make(chan interface{})
go func() {
defer fmt.Println("dowork exited")
defer close(terminated)
for {
select {
case s := <-strings:
fmt.Println(s)
case <-done: // 2
return
}
}
}()
return terminated
}
done := make(chan interface{})
terminated := dowork(done, nil)
go func() { // 3
time.Sleep(1 * time.Second)
fmt.Println("cancle dowork goroutine")
close(done)
}()
<-terminated // 4
fmt.Println("Done")
  • 在 1 处将完成的 channel 传递给 dowork
  • 在 2 处检查 done channel 是否已经发出信号,如果发出信号从 goroutine 返回
  • 在 3 处创建了另一个 goroutine,在 1s 后关闭 donechannel。
  • 这就是加入从 main goroutine 的 dowork 中产生的 goroutine 的地方。

输出:

1
2
3
cancle dowork goroutine
dowork exited
Done

这次还是传递了 nil,但是不同的是还有一个 done channel。这次 goroutine 成功推出了,与之前的例子不同的是加入了 2 个 goroutine 但是没有死锁。因为我们创建了第三个 goroutine 在 dowork 执行 1s 之后取消 dowork 中的 goroutine,这样就简单的消除了一个 goroutine 泄露。

如果一个 goroutine 阻塞了向 channel 进行写入的请求?
example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
newRandStream := func() <-chan int {
randstream := make(chan int)
go func() {
defer fmt.Println("newRandStream exited") // 1
defer close(randstream)
for {
randstream <- rand.Int():
}
}()
return randstream
}
RandStream := newRandStream(done)
for i := 0; i < 3; i++ {
fmt.Printf("%d : %d \n", i, <-RandStream)
}
time.Sleep(1 * time.Second)
  • 1 处在 goroutine 成功终止时打印出一条信息
    然而运行这段程序你会发现 defer 的打印永远不会执行,在循环第三次迭代之后,我们的 goroutine 试图将下一个随机整数发送到不再被读取的 channel,我们无法告诉生产者它可以停止。解决方案和上一个也是类似的,为生产者 goroutine 提供一个通知它退出的 channel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
newRandStream := func(done chan interface{}) <-chan int {
randstream := make(chan int)
go func() {
defer fmt.Println("newRandStream exited")
defer close(randstream)
for {
select {
case randstream <- rand.Int():
case <-done:
return
}
}
}()
return randstream
}
done := make(chan interface{})
RandStream := newRandStream(done)
for i := 0; i < 3; i++ {
fmt.Printf("%d : %d \n", i, <-RandStream)
}
close(done)
time.Sleep(1 * time.Second)

再一次运行程序,会看到 defer 的打印语句执行了,goroutine 已经被正确的清理了,我们可以规定一个约定:如果 gorotuine 负责创建 goroutine,它也负责确保它可以停止 goroutine。