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

5.1.3 sync.WaitGroup

在编程实战中,不可能在main函数中通过休眠来等待goroutine执行结束,这样既浪费资源也不优雅。Go语言肯定会提供更为优雅高效的方式来结束goroutine,下面就来进行介绍。

下面通过简短清晰的代码启动多个goroutine,示例代码是Go语言的标准包中sync包的使用:


book/ch05/5.1/sync/main.go
1. package main
2.
3. import (
4.     "fmt"
5.     "sync"
6. )
7.
8. func main() {
9.      var wg sync.WaitGroup
10.     for i:=0;i<20;i++{
11.         wg.Add(1)
12.         go func(x int) {
13.             defer wg.Done()
14.             fmt.Print(" ",x)
15.         }(i)
16.     }
17.     fmt.Printf("\n%#v\n",wg)
18.     wg.Wait()
19.     fmt.Println("\nThe End!")
20. }
21.
22. //以下为程序两次执行结果
23.  1 10 5
24. sync.WaitGroup{noCopy:sync.noCopy{}, state1:[3]uint32{0x0, 0x14, 0x0}}
25.  19 11 12 13 14 15 16 17 18 7 6 0 8 3 2 9 4
26. The End!
27.
28.  7 10 3 9 2 19 6 11 5 8 14 15 16 17 13 1 0
29. sync.WaitGroup{noCopy:sync.noCopy{}, state1:[3]uint32{0x0, 0x14, 0x0}}
30.  4 12 18
31. The End!

在本例中,定义了一个sync.WaitGroup类型的变量wg,其实sync.WaitGroup就是本示例代码的核心。sync包可以让我们知道何时所有的goroutine执行完成,可以不再使用time包,在第3行至第6行的导入部分确实也没有time包。

下面来详细看一下sync.WaitGroup的结构体,其定义如下:


type WaitGroup struct{
    noCopy noCopy
    state1 [12]byte
    sema uint12
}

结构体很简单,只有三个字段,其中state1字段是一个计数器,其用法也很好理解。每当有一个goroutine运行的时候就调用Add方法给计数器加1,待一个goroutine运行完后,通过Done方法为计数器减1,然后使用Wait方法等待计数器的数变为0。

需要注意的是,Add方法和Wait方法都是在主goroutine中执行的,而Done的执行却是在启动的goroutine中。第13行是调用Done方法,不过要注意该方法前面的defer。defer关键字后面跟函数调用语句,defer的触发机制包含下面三种情况:

▪defer所在的函数返回时,触发defer后面的函数调用语句。

▪defer所在的函数执行到末尾时,触发defer后面的函数调用语句。

▪defer所在的函数报panic时,触发defer后面的函数调用语句。

在本例中,goroutine执行到末尾时,触发调用Done方法。关于defer更多的用法会在第6章进行更详细的介绍。

注意

当sync.WaitGroup作为参数传递到函数内且调用Done方法的时候,一定不要忘记函数的值传递特性,这里应该传递sync.WaitGroup变量的地址,如果传递值则会复制另一个sync.WaitGroup类型变量出来,和函数外的那个变量就没有关系了,即便调用了Done方法,最后还是会报DeadLock错误。