WaitGroup底层原理

WaitGroup

核心组件

WaitGroup 结构体内部主要包含以下两个核心部分(在较新的 Go 版本中,实现有所优化,但基本思想一致):

  1. counter (计数器):这是一个整数,用来记录需要等待的 goroutine 的数量。
    • 当我们调用 Add(n) 方法时,这个计数器会增加 n
    • 当我们调用 Done() 方法时(通常在每个 goroutine 完成任务时调用),这个计数器会减 1。
  2. waiters (等待者计数器) 和 semaphore (信号量)
    • 当一个 goroutine 调用 Wait() 方法时,如果 counter 的值大于 0,那么这个 goroutine 就会被阻塞。
    • waiters 用来记录当前有多少个 goroutine 因为调用 Wait() 而被阻塞。
    • semaphore 是一个信号量,用于实现阻塞和唤醒的机制。当 counter 变为 0 时,所有在 Wait() 上阻塞的 goroutine 都会通过这个信号量被唤醒。

工作流程

  1. 初始化:

    • 当你创建一个 WaitGroup 实例时,它的 counter 初始值为 0。
  2. 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 增加的数量。
  3. Done() 方法:

    • 这个方法实际上是 Add(-1) 的一个封装。
    • 它表示一个被等待的 goroutine 已经完成了它的任务。
    • 每当一个 goroutine 完成时,它应该调用 Done()
  4. Wait() 方法:

    • 当一个 goroutine 调用 Wait() 时:
      • 它首先会检查 counter 的值。
      • 如果 counter 为 0,表示所有需要等待的 goroutine 都已经完成了,Wait() 方法会立即返回。
      • 如果 counter 大于 0,表示还有 goroutine 尚未完成:
        • waiters 计数会增加,表示有一个新的 goroutine 开始等待。
        • 该 goroutine 会在 semaphore 上阻塞,直到被唤醒。
    • counter 的值从一个正数变为 0 时(通常是由于最后一个活动的 goroutine 调用了 Done()),所有在 semaphore 上等待的 goroutine 都会被唤醒,然后它们可以从 Wait() 方法返回并继续执行。

总结

  • 主 goroutine (或者任何需要等待的 goroutine) 调用 Add(n) 来设置需要等待的 goroutine 数量。
  • 然后,主 goroutine 启动 n 个子 goroutine。
  • 每个子 goroutine 在完成其工作后,调用 Done()
  • 主 goroutine 调用 Wait(),它会一直阻塞,直到所有 n 个子 goroutine 都调用了 Done(),使得内部计数器减到 0。

内部实现细节 (基于 Go 1.18+ 的 state1state2 字段)

在较新的 Go 版本中,WaitGroup 的实现进行了一些优化,使用一个 64 位整数 state1 来同时存储 counter (高32位) 和 waiters (低32位),并通过原子操作来更新它们,以提高效率和避免锁。state2 则用作信号量。

  • state1 的高32位是 counter
  • state1 的低32位是 waiters
  • state2 是信号量,sync.runtime_Semacquiresync.runtime_Semrelease 用于阻塞和唤醒。

这种设计允许通过原子操作同时修改 counterwaiters,减少了锁的争用。

使用 WaitGroup 的注意事项:

  1. Add 的调用时机: 必须在 goroutine 启动前调用 Add 来增加计数,或者确保在 Wait 返回后且下次 Wait 前调用。如果在 goroutine 内部调用 Add,可能会导致 WaitAdd 执行前就判断计数为0而提前返回,引发竞争。
  2. Done 的调用: 确保每个通过 Add 计数的 goroutine 最终都会调用 Done,否则 Wait 会永久阻塞。通常使用 defer wg.Done() 来确保即使发生 panic,Done 也会被调用。
  3. 不要拷贝 WaitGroup: WaitGroup 在首次使用后不应该被拷贝。如果你需要传递 WaitGroup,应该传递它的指针。

打 赏