Go微服务实战
上QQ阅读APP看书,第一时间看更新

5.2.6 超时检查

使用select可以方便地完成goroutine的超时检查。超时就是指某个goroutine由于意外退出,导致另一方的goroutine阻塞,从而影响主goroutine,可以参考5.2.4节book/ch05/5.2/buffer/main.go例子中提出的思考题,该问题就属于超时。

注意

当goroutine向某个无缓存通道发送数据,而没有其他goroutine从该无缓存通道接收数据时,发送的goroutine就会阻塞,这种情况叫作goroutine泄露。goroutine泄露是造成超时的最常见原因。

对于超时这个问题要特别注意,所以本节主要介绍的是如何对超时的情况进行检查。这里所说的超时检查方式,技术上是使用select+time.After()来实现的。

先来看一个比较简单的检查方式,示例代码如下:


book/ch05/5.2/timeout1/main.go
1. package main
2.
3. import (
4.     "fmt"
5.     "time"
6. )
7.
8. func main() {
9.      //匿名函数中的代码块休眠4s
10.     ch1 := make(chan string)
11.     go func() {
12.         time.Sleep(4*time.Second)
13.         ch1 <- "ch1 si ready!"
14.     }()
15.     //注意time.After的使用,而且休眠时间为2s
16.     select {
17.     case mess := <- ch1:
18.         fmt.Println(mess)
19.     case t := <- time.After(2*time.Second):
20.         fmt.Println("ch1 timeout!",t)
21.     }
22.     //匿名函数休眠4s
23.     ch2 := make(chan string)
24.     go func() {
25.         time.Sleep(4*time.Second)
26.         ch2 <- "ch2 is ready!"
27.     }()
28.     //time.After内等待5s
29.     select {
30.     case mess := <- ch2:
31.         fmt.Println(mess)
32.     case t := <- time.After(5*time.Second):
33.         fmt.Println("ch2 timeout",t)
34.
35.     }
36. }
37. //以下是程序执行结果
38. ch1 timeout! 2019-08-21 09:48:58.086281 +0800 CST m=+2.004157554
39. ch2 is ready!

第9行至第14行,先定义一个通道,然后启动一个goroutine运行匿名函数。匿名函数先休眠4秒,然后将一个字符串写入通道ch1。

第15行至第21行,select内写了两个case,一个是从ch2读取信息,如果可以读取,则打印信息;另一个是通过time.After函数读取2秒后的时间。注意,整个select的前2秒是阻塞状态,因为前面的goroutine要休眠4s再向通道写入数据,而select只等待2秒就会执行第二个case。结合第38行的执行结果可知,执行的确实是第二个case。

第22行至第27行,定义一个string型的通道ch2,然后启动一个goroutine来运行匿名函数,匿名函数先休眠4秒,然后向ch2通道写入字符串数据。

第29行至第36行,select内写了两个case,第一个是从ch2内读取信息,等ch2可读的时候执行该代码块;第二个是从执行到select开始5秒后执行该代码块,注意前面的匿名函数是休眠4秒,所以该代码块应该没有机会执行。结合第39行的运行结果可知,执行的是第一个case。

到这里,第一种用法就介绍完了,也就是在select内使用time.After函数,设定最长等待时间。因为select内如果没有满足条件的case则进入阻塞状态,而time.After的作用就是设定等待时间,等待时间到了,如果select内还没有满足条件的case,则执行该case。

通过这种方法虽然可以解决超时问题,但是显然不够灵活,这种硬编码的方式在编程中是不常用的。也是基于这种缺点,需要一些更灵活的解决超时问题的方式。下面来看一下示例代码:


book/ch05/5.2/timeout1/main.go
1. package main
2.
3. import (
4.     "fmt"
5.     "math/rand"
6.     "sync"
7.     "time"
8. )
9.
10. func main() {
11.     var wg sync.WaitGroup
12.     wg.Add(1)
13.     //需要初始化随机数的资源库, 如果不执行这行, 不管运行多少次都返回同样的值
14.     rand.Seed(time.Now().UnixNano())
15.     no := rand.Intn(6)
16.     no *= 1000
17.     du := time.Duration(int32(no))*time.Millisecond
18.     fmt.Println("timeout duration is:",du)
19.     wg.Done()
20.     if isTimeout(&wg,du){
21.         fmt.Println("Time out!")
22.     }else {
23.         fmt.Println("Not time out")
24.     }
25. }
26.
27. func isTimeout(wg *sync.WaitGroup,du time.Duration) bool  {
28.     ch1 := make(chan int)
29.     go func() {
30.         time.Sleep(3*time.Second)
31.         defer close(ch1)
32.         wg.Wait()
33.     }()
34.     select {
35.     case <-ch1:
36.         return false
37.     case <- time.After(du):
38.         return true
39.     }
40. }

这是一种模拟,让程序的等待时间可以根据传入参数进行不同的超时时长判断。这里是用随机数来模拟时长的,真实的项目中可以根据配置参数或者统计参数在运行时传递到函数中。本部分代码就不再做逐行解读,请读者自行实验。