谈到goroutine并发超时控制,一搜很容易看到下面这样的代码。
func TestGoroutine3(t *testing.T) {done := make(chan struct{}, 0)go func() {// do somethingtime.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")
}