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错误。