今天我们看一个有意思的go routine案例,从而了解golang中的sync包的waitgroup用法。
01
一个简单案例
我们写一个简单的for循环,循环体里面写上go routine,启动多个goroutine来打印循环变量i
func main() {
for i := 0; i < 200; i++ {
go fmt.Println("routine:", i)
fmt.Println(i)
}
}
上述案例可以得到下面输出:
...省略
21
22
routine: 0
routine: 3
routine: 1
routine: 13
23
24
...省略
198
199
routine: 189
routine: 198
routine: 188
...
由于输出了200个数字,太长了占满页面,结果的前后我省略了一部分。
上面的例子不难看出来:
1、因为go routine是协程,所以存在这个routine的先后顺序无法保证,可能后面的循环先输出。例如routine3出现在routine1的前面。
2、go routine的运行需要时间,循环到第22次的时候,routine0的结果才输出来。循环结束的时候,还有一些go routine没有执行完。
为了保证go routine全部执行完毕并输出,我们往往会使用sleep 函数阻塞主程序main,从而等待go routine运行。上面的程序会改成:
func main() {
for i := 0; i < 200; i++ {
go fmt.Println("routine:", i)
fmt.Println(i)
}
time.Sleep(time.Second * 1)
}
...
上述程序非常简单,是一个简单循环,循环体中只有打印语句,没有其他的语句。
但是如果循环中的逻辑特别多的时候,我们应该sleep多久,才能保证所有的go routine执行完毕呢???答案是不确定。
02
利用channel
为了解决上述问题,我们引入了管道channel。channel默认是阻塞的。当数据被发送到channel时会发生阻塞,直到有其他go routine从该channel中读取数据。当从channel读取数据时,读取也会被阻塞,直到其他go routine将数据写入该channel。
func main(){
c := make(chan bool, 200)
for i := 0; i < 200; i++ {
go func(i int) {
fmt.Println(i)
c <- true
}(i)
}
for i := 0; i < 200; i++ {
<-c
}
}
上述代码中,我们利用管道天然的阻塞特性,先初始化一个channel,channel中传递bool类型的值,然后运行go routine,并在go routine结束的时候,将true塞入channel。
在第一个循环结束之后(并非go routine结束),我们开始遍历这个channel,从channel中吐出来200个bool类型的值,如果不够200个,程序将天然阻塞,这就能够保证,所有的go routine都被执行完了。
channel的方法能够解决我们上述的time.sleep时间不确定的问题。
但是channel有一个缺点,就是比较耗费内存。假设我们的循环终止条件上限是10w或者100w,那么我们不得不申请同样大小的channel。
03
利用sync包工具sync.WaitGroup
除了channel之外,还可以使用sync.WaitGroup来解决这个问题。
WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。其中:
Add(n) 把计数器设置为n ,
Done() 每次把计数器执行减一操作 ,
wait() 会阻塞代码的运行,直到计数器的值减为0。
func main() {
wg := sync.WaitGroup{}
wg.Add(200)
for i := 0; i < 200; i++ {
go func(i int) {
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
}
我们遵照上面的描述,先设置计数器为200,然后go func中执行Done,对计数器进行减一操作,然后最后利用wait函数,捕获计数器的值为0的时候,结束程序。
相比较而言,sync.WaitGroup工具会更加轻量。