管理Goroutine的生命周期
Rule 1:永远不要开始一个你无法确他何时退出的协程
一个无法退出的goroutine可能是阻塞在某些channel上了,对于这种永久阻塞,GC并不会将goroutine停止,并释放内存,这将导致goroutine leak 内存泄漏。
Goroutines can leak by blocking on channel sends or receives: the garbage collector will not terminate a goroutine even if the channels it is blocked on are unreachable.
https://github.com/golang/go/wiki/CodeReviewComments#goroutine-lifetimes
即使goroutine并没有阻塞在channel,而是在期望他已关闭的场景下继续运行。这将可能导致很难诊断的错误,可能会引起panic或者是data races,也可能会导致内存飙升等一系列头疼问题。
如何管理goroutine声明周期?可以尝试一下的tips:
- 尽量让你的goroutine代码简单清晰,简单到一眼就能看穿其生命周期。
- 对于每个goroutine方法,都应将其生命周期注释清楚,说明其退出的条件。
- 在父级goroutine中使用context来管理子goroutine的声明周期。
- 使用专门的channel通知goroutine结束
channel通知退出模式
以下用我最喜欢的channel通知退出模式来举例说明:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
type add struct {
done chan struct{} //退出信号
in chan int
out chan int
wg sync.WaitGroup
}
//每一秒往in里添加一个随机数,当close(a.done)时退出
func (a *add) input() {
defer a.wg.Done()
for {
select {
case <-a.done:
return
case <-time.After(1 * time.Second):
a.in <- rand.Intn(100)
}
}
}
//从in里取出内容,并+1,写入out,当close(a.done)时退出
func (a *add) process() {
defer a.wg.Done()
for {
select {
case <-a.done:
return
case i := <-a.in:
i++
a.out <- i
}
}
}
//打印out里的值,当close(a.done)时退出
func (a *add) output() {
defer a.wg.Done()
for {
select {
case <-a.done:
return
case o := <-a.out:
fmt.Println(o)
}
}
}
func main() {
a := &add{done: make(chan struct{}), in: make(chan int, 5), out: make(chan int, 5)}
a.wg.Add(3)
go a.input()
go a.process()
go a.output()
<-time.After(5 * time.Second) //5秒后结束
close(a.done)
a.wg.Wait() //等待goroutine全部结束
fmt.Println("结束")
}
在上例的程序中,我们启用了3个协程,设置定时器在5秒后全部结束,通过close的广播机制,