核心组件
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,应该传递它的指针。
如果您喜欢我的文章,请点击下面按钮随意打赏,您的支持是我最大的动力。
最新评论