协程同步
在实际项目开发过程中经常会遇到并发需要协程同步的场景,经常看到有人会问如何等待主协程中创建的协程执行完毕之后再结束主协程,例如下面代码,通过起100个协程实现并发打印的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 | package main import ( "fmt" ) func main() { for i := 0; i < 100 ; i++{ go func(i int) { fmt.Println("Goroutine ",i) }(i) } } |
执行以上代码很可能看不到输出,也可能只执行了部分协程,因为有可能这100个协程还没得到执行主协程已经结束了,或者执行了部分协程主协程执行完了,而主协程结束时会结束所有其他协程。解决办法是可以在main函数结尾加上sleep()等待:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package main import ( "fmt" "time" ) func main() { for i := 0; i < 100 ; i++{ go func(i int) { fmt.Println("Goroutine ",i) }(i) } time.Sleep(time.Second * 1) // 睡眠1秒,等待上面两个协程结束 } |
主线程为了等待goroutine都运行完毕,不得不在程序的末尾使用time.Sleep() 来睡眠一段时间,等待其他线程充分运行。对于简单的代码,多个for循环可以在1秒之内运行完毕,time.Sleep() 也可以达到想要的效果。
但是对于实际生活的大多数场景来说,1秒是不够的,并且大部分时候我们都无法预知for循环内代码运行时间的长短。这时候就不能使用time.Sleep() 来完成等待操作了。这并不是完美的解决方法,如果这两个协程中包含复杂的操作,可能很耗时间,就无法确定需要睡眠多久,当然可以用管道实现同步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package main import ( "fmt" ) func main() { ch := make(chan struct{}) count := 100 // count 表示活动的协程个数 for i := 0; i < 100 ; i++{ go func(i int) { fmt.Println("Goroutine ",i) ch <- struct{}{} // 协程结束,发出信号 }(i) } for range ch { // 每次从ch中接收数据,表明一个活动的协程结束 count-- // 当所有活动的协程都结束时,关闭管道 if count == 0 { close(ch) } } } |
上面的解决方案是比较完美的方案,首先可以肯定的是使用管道是能达到我们的目的的,而且不但能达到目的,还能十分完美的达到目的。但是管道在这里显得有些大材小用,因为它被设计出来不仅仅只是在这里用作简单的同步处理,在这里使用管道实际上是不合适的。而且假设我们有一万、十万甚至更多的for循环,也要申请同样数量大小的管道出来,对内存也是不小的开销。Go提供了更简单的方法——使用sync.WaitGroup。WaitGroup顾名思义,就是用来等待一组操作完成的。WaitGroup内部实现了一个计数器,用来记录未完成的操作个数,它提供了三个方法,Add()用来添加计数。Done()用来在操作结束时调用,使计数减一。Wait()用来等待所有的操作结束,即计数变为0,该函数会在计数不为0时等待,在计数为0时立即返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup wg.Add(100) // 因为有两个动作,所以增加2个计数 for i := 0; i < 100 ; i++{ go func(i int) { fmt.Println("Goroutine ",i) wg.Done() // 操作完成,减少一个计数 }(i) } wg.Wait() // 等待,直到计数为0 } |
可见用sync.WaitGroup是最简单的方式。
注意事项
1. 计数器不能为负值
我们不能使用Add() 给wg 设置一个负值,否则代码将会报错:
1 2 3 4 5 6 7 | panic: sync: negative WaitGroup counter goroutine 1 [running]: sync.(*WaitGroup).Add(0xc042008230, 0xffffffffffffff9c) D:/Go/src/sync/waitgroup.go:25 +0x1d0 main.main() D:/code/go/src/test-src/2-Package/sync/waitgroup/main.go:10 +0x54 |
同样使用Done() 也要特别注意不要把计数器设置成负数了。
2. WaitGroup对象不是一个引用类型
WaitGroup对象不是一个引用类型,在通过函数传值的时候需要使用地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | func main() { wg := sync.WaitGroup{} wg.Add(100) for i := 0; i < 100; i++ { go f(i, &wg) } wg.Wait() } // 一定要通过指针传值,不然进程会进入死锁状态 func f(i int, wg *sync.WaitGroup) { fmt.Println(i) wg.Done() } |
相互学习。共同进步。