Golang 并发编程(七):Golang 中同步工具—WaitGroup
- 陈大剩
- 2025-02-26 21:48:43
- 221
WaitGroup 介绍
sync.WaitGroup
类型是并发安全的,也是开箱即用的。例如,在声明 var wg sync.WaitGroup
就可以直接使用 wg
变量了。sync.WaitGroup
与前面讨论的几个同步工具一样,它一旦被真正使用就不能被复制了。
WaitGroup
类型拥有三个指针方法:Add
、Done
和 Wait
。你可以想象该类型中有一个计数器,它的默认值是 0
。我们可以通过调用该类型值的 Add
方法来增加,或者减少这个计数器的值。具体例子如下:
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
fmt.Println("第一个协程")
}()
go func() {
defer wg.Done()
fmt.Println("第二个协程")
}()
wg.Wait()
这个例子中,一定要例子中的 2 个 goroutine
同时做完,才算是完成,先做好的就要等着其他未完成的,所有的 goroutine
要都全部完成才可以。
WaitGroup 生命周期
我们来玩一些有意思的事情,例子如下:
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
fmt.Println("第一个协程")
}()
wg.Wait()
首先猜猜这个代码会出现什么样结果?
第一个协程
fatal error: all goroutines are asleep - deadlock!
没错,会报错,为什么呢?这就得说一下,WaitGroup
的生命周期了。在 WaitGroup
一个技术周期中,总会是以 0 开始,然后再以 0 结束,当归为0才会唤醒 wg.Wait()
,否则 wg.Wait()
会一直阻塞,最终造成死锁(也就是上面的例子)。
接下来看看这个例子:
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("第一个协程")
}()
wg.Wait() // 第一个计数周期结束
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("第二个协程")
}()
wg.Wait() // 第二个计数周期结束
输出如下:
第一个协程
第二个协程
这个例子中,WaitGroup
一共进行了两轮生命周期,生命周期总是以 Add(x)
开始,wg.Wait()
结束。如果一个此类值的 Wait
方法在它的某个计数周期中被调用,那么就会立即阻塞当前的 goroutine
,直至这个计数周期完成。在这种情况下,该值的下一个计数周期,必须要等到这个 Wait
方法执行结束之后,才能够开始。
注意:如果在一个此类值的Wait方法被执行期间,跨越了两个计数周期,那么就会引发一个 panic。
sync.Once 只执行一次
sync
包中还有一个特殊的结构体类型 sync.Once
,sync.Once
拥有一个 Do
方法。例子如下:
var one sync.Once
one.Do(func() {
fmt.Println("只会执行一次")
})
对于同一个 sync.Once
类型的值的 Do 方法的有效调用次数永远会是 1。也就是说,无论用这个方法多少次,也无论在多次传递给它的参数值是否相等,都仅会调用一次,具体看下面这个例子:
var one sync.Once
var count int
for i := 0; i < 10; i++ {
one.Do(func() {
count++
})
}
fmt.Println(count)
输出:
1
这个例子中,无论我们如何执行,这里输出都只会输出一次。