核心组件
WaitGroup
结构体内部主要包含以下两个核心部分(在较新的 Go 版本中,实现有所优化,但基本思想一致):
counter
(计数器):这是一个整数,用来记录需要等待的 goroutine 的数量。
Add(n)
方法时,这个计数器会增加 n
。Done()
方法时(通常在每个 goroutine 完成任务时调用),这个计数器会减 1。waiters
(等待者计数器) 和 semaphore
(信号量):
Wait()
方法时,如果 counter
的值大于 0,那么这个 goroutine 就会被阻塞。waiters
用来记录当前有多少个 goroutine 因为调用 Wait()
而被阻塞。semaphore
是一个信号量,用于实现阻塞和唤醒的机制。当 counter
变为 0 时,所有在 Wait()
上阻塞的 goroutine 都会通过这个信号量被唤醒。工作流程
初始化:
WaitGroup
实例时,它的 counter
初始值为 0。Add(delta int)
方法:
counter
的值。delta
可以是正数(表示要增加等待的 goroutine 数量)或负数(通常通过 Done()
方法间接实现)。Add
方法必须在对应的 goroutine 启动之前调用,或者在 Wait
方法返回之后、下一次 Wait
之前调用,以避免竞争条件。counter
加上 delta
后变为 0,并且此时 waiters
大于 0(即有 goroutine 正在 Wait()
),那么 Add
方法会负责唤醒所有等待的 goroutine。counter
加上 delta
后变为负数,Add
方法会引发一个 panic,因为这通常意味着 Done()
被调用的次数超过了 Add
增加的数量。Done()
方法:
Add(-1)
的一个封装。Done()
。Wait()
方法:
Wait()
时:
counter
的值。counter
为 0,表示所有需要等待的 goroutine 都已经完成了,Wait()
方法会立即返回。counter
大于 0,表示还有 goroutine 尚未完成:
waiters
计数会增加,表示有一个新的 goroutine 开始等待。semaphore
上阻塞,直到被唤醒。counter
的值从一个正数变为 0 时(通常是由于最后一个活动的 goroutine 调用了 Done()
),所有在 semaphore
上等待的 goroutine 都会被唤醒,然后它们可以从 Wait()
方法返回并继续执行。总结
Add(n)
来设置需要等待的 goroutine 数量。n
个子 goroutine。Done()
。Wait()
,它会一直阻塞,直到所有 n
个子 goroutine 都调用了 Done()
,使得内部计数器减到 0。内部实现细节 (基于 Go 1.18+ 的 state1
和 state2
字段)
在较新的 Go 版本中,WaitGroup
的实现进行了一些优化,使用一个 64 位整数 state1
来同时存储 counter
(高32位) 和 waiters
(低32位),并通过原子操作来更新它们,以提高效率和避免锁。state2
则用作信号量。
state1
的高32位是 counter
。state1
的低32位是 waiters
。state2
是信号量,sync.runtime_Semacquire
和 sync.runtime_Semrelease
用于阻塞和唤醒。这种设计允许通过原子操作同时修改 counter
和 waiters
,减少了锁的争用。
使用 WaitGroup
的注意事项:
Add
的调用时机: 必须在 goroutine 启动前调用 Add
来增加计数,或者确保在 Wait
返回后且下次 Wait
前调用。如果在 goroutine 内部调用 Add
,可能会导致 Wait
在 Add
执行前就判断计数为0而提前返回,引发竞争。Done
的调用: 确保每个通过 Add
计数的 goroutine 最终都会调用 Done
,否则 Wait
会永久阻塞。通常使用 defer wg.Done()
来确保即使发生 panic,Done
也会被调用。WaitGroup
: WaitGroup
在首次使用后不应该被拷贝。如果你需要传递 WaitGroup
,应该传递它的指针。
如果您喜欢我的文章,请点击下面按钮随意打赏,您的支持是我最大的动力。
最新评论