goroutine的退出与泄露:如何检测和预防

共 548字,需浏览 2分钟

 ·

2020-07-29 05:17






goroutine的退出机制






Go中,goroutine是否结束执行(退出)是由其自身决定,其他goroutine只能通过消息传递的方式通知其关闭,而并不能在外部强制结束一个正在执行的goroutine。当然有一种特殊情况会导致正在运行的goroutine会因为其他goroutine的结束而终止,即main函数退出。


在Go中常见的控制goroutine退出方式有以下几种


/main函数的结束

func G1()  {
 time.Sleep(time.Second)
 fmt.Println("G1 exit")
}

func main() {
 go  G1()
 fmt.Println("main exit")
}

go run main.go
main exit


如上所示,程序未等G1执行完毕,即随着main函数的退出而停止执行。


/context通知退出

func G1(ctx context.Context)  {
 num := 0
 for {
  select {
  case <-ctx.Done():
   fmt.Println("G1 exit")
   return
  case <-time.After(time.Second):
   num++
   fmt.Printf("G1 wait times: %d\n", num)
  }
 }
}

func main() {
 ctx, cancel := context.WithCancel(context.Background())
 go G1(ctx)
 time.Sleep(3*time.Second)
 cancel()
 time.Sleep(time.Second)
 fmt.Println("main exit")
}

go run main.go
G1 wait times: 1
G1 wait times: 2
G1 exit
main exit


/panic异常结束

func G1() {
 defer func() {
  if err := recover(); err != nil {
   fmt.Printf("G1 exit by panic: %v\n", err)
  }
 }()

 _, err := os.Open("notExistFile.txt")
 if err != nil {
  panic(err)
 }
 fmt.Println("G1 exit naturally")
}

func main() {
 go G1()
 time.Sleep(time.Second)
 fmt.Println("main exit")
}

go run main.go
G1 exit by panic: open notExistFile.txt: no such file or directory
main exit


上面函数G1中defer函数使用了recover来捕获panic,当panic发生时可使goroutine拿回控制权,确保程序不会将panic传递到goroutine调用栈顶部后引起崩溃。


/执行完毕后退出

func G1() {
 for i := 0; i < 10000; i++ {
  //do some work
 }
 fmt.Println("G1 exit")
}

func main() {
 go G1()
 time.Sleep(time.Second)
 fmt.Println("main exit")
}

go run main.go
G1 exit
main exit


goroutine里的任务执行完毕,即结束。







什么是goroutine泄露






如果你启动了一个goroutine,但并没有按照预期的一样退出,直到程序结束,此goroutine才结束,这种情况就是 goroutine 泄露。当 goroutine 泄露发生时,该 goroutine 的栈一直被占用而不能释放,goroutine 里的函数在堆上申请的空间也不能被垃圾回收器回收。这样,在程序运行期间,内存占用持续升高,可用内存越来也少,最终将导致系统崩溃。


大多数情况下,引起goroutine泄露的原因有两类:channel阻塞;goroutine陷入死循环。


/channel阻塞

1. 从channel里读,但是没有写

func G1() {
 c := make(chan int)
 go func() {
  <-c
 }()
 time.Sleep(2*time.Second)
 fmt.Println("G1 exit")
}

func main() {
 go G1()

 c := time.Tick(time.Second)
 for range c {
  fmt.Printf("goroutine [nums]: %d\n", runtime.NumGoroutine())
 }
}

go run main.go
goroutine [nums]: 3
goroutine [nums]: 3
G1 exit
goroutine [nums]: 2
goroutine [nums]: 2
...


2. 向已满的channel里写,但是没有读


func G2(size int) {
 c := make(chan int, size)
 go func() {
  <-c
 }()

 go func() {
  for i := 0; i < 10; i++ {
   c <- i
  }
 }()

 fmt.Println("G2 exit")
}

var size = flag.Int("c",0"define channel size")

func main() {
 flag.Parse()
 go G2(*size)

 c := time.Tick(time.Second)
 for range c {
  fmt.Printf("goroutine [nums]: %d\n", runtime.NumGoroutine())
 }
}

go run main.go -c 2
G2 exit
goroutine [nums]: 2
goroutine [nums]: 2
...
go run main.go -c 11
G2 exit
goroutine [nums]: 1
goroutine [nums]: 1
...


/死循环

当代码里循环的退出条件不可达时,会令该goroutine进入死循环中,进而导致资源一直无法释放,引起泄露。在实际项目中,往往死循环会发生在一些后台的常驻服务中。







goroutine泄露的预防和检测






/预防

1. 最重要的一点,在创建goroutine时,就应该知道goroutine啥时能结束。


2. channel引起的goroutine泄露问题,主要是看在channel阻塞goroutine时,该goroutine的阻塞是正常的,还是可能导致协程永远没有机会执行。若是后者,则极大可能会造成协程泄露。


channel的实际使用中,常用的两种模型:生产者-消费者模型;master-worker模型。一般的解决方案是:当主线程结束时,告知生产协程,生产协程得到通知后,进行清理工作然后退出;为每个worker任务制定超时,当超时触发,返回给master超时信息,并结束该worker协程。具体代码方案是使用上下文context。


3. 实现循环语句时必须清晰地知道退出循环的条件,避免死循环。


/检测

1. Go提供的pprof工具。

2. 利用runtime.NumGoroutine接口,实时查看程序中运行的goroutine数。

3. 开源三方profiling库。

gops,地址:https://github.com/google/gops

goleak,地址:https://github.com/uber-go/goleak





推荐阅读



学习交流 Go 语言,扫码回复「进群」即可


站长 polarisxu

自己的原创文章

不限于 Go 技术

职场和创业经验


Go语言中文网

每天为你

分享 Go 知识

Go爱好者值得关注


浏览 71
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报