谈到goroutine并发超时控制,一搜很容易看到下面这样的代码。
func TestGoroutine3(t *testing.T) {
done := make(chan struct{}, 0)
go func() {
// do something
time.Sleep(time.Second)
fmt.Println("server")
done <- struct{}{}
fmt.Println("send done success!")
}()
select {
case <- done:
fmt.Println("done")
case <- time.After(time.Millisecond * 100):
fmt.Println("time out")
}
}
这段代码很简,通过channel+select来控制超时,如果是一运行完就结束。那么没有问题。
但是!!,如果是服务器类的程序就有问题,会存在goroutine泄漏,如果服务长期不重启,那么一个次请求如果发生了超时就会泄漏goroutine。为什么?因为done这个channel无缓冲,超时后,当子goroutine执行到done<-struct{}{}将会一直阻塞,导致goroutine不会被回收。
网上太多这样类似的代码,都不提这个问题。正确的做法是是什么呢?在超时后,我们需要回收开启的goroutine。
方法一:超时后直接关闭done这个channel,但是这会引发Panic,没关系,加个defer接住就行
func TestGoroutine(t *testing.T) {
done := make(chan struct{}, 0)
go func() {
defer func() {
err := recover()
fmt.Println(err)
}()
time.Sleep(time.Second)
fmt.Println("server")
done <- struct{}{}
fmt.Println("send done success!")
}()
select {
case <- done:
fmt.Println("done")
case <- time.After(time.Millisecond * 100):
close(done)
fmt.Println("time out")
}
// 停一段时间看超时goroutine是否继续运行
time.Sleep(time.Second*3)
//<-done
//fmt.Println("3 s, done")
}
方法二:使用context,使用context来取消goroutine,其大概作用机制就是引入一个新的channel,假设叫done2,在开启的goroutine中使用select从done中接受值,当超时时,关闭这个channel,接受就不会再阻塞,从而可以正常回收goroutine。使用context或者自己make一个channel都可以,关于context可以看我的另一篇文章,下面给出context的例子,使用channel也可以轻易实现。
func TestGoroutine2(t *testing.T) {
done := make(chan struct{}, 0)
ctx, cancelFunc := context.WithCancel(context.Background())
go func(ctx context.Context) {
time.Sleep(time.Second)
fmt.Println("server")
select {
case done <- struct{}{}:
case <-ctx.Done():
}
fmt.Println("send done success!")
}(ctx)
select {
case <- done:
fmt.Println("done")
case <- time.After(time.Millisecond * 100):
cancelFunc()
fmt.Println("time out")
}
// 停一段时间看超时goroutine是否继续运行
time.Sleep(time.Second*3)
//<-done
//fmt.Println("3 s, done")
}
方法三(更新):更简单的方法实际上是将channel大小设为1,这样发送就不会阻塞了。。