sync.WaitGroup 是 Go 语言标准库中 sync 包提供的一个同步原语,用于等待一组 goroutine 完成。它通过计数器机制来协调多个 goroutine 的执行,使得主程序或其他 goroutine 能够在所有的 goroutine 完成其任务后再继续执行。以下是对 sync.WaitGroup 的详细讲解,包括其基本用法、工作原理以及使用示例。

1. 基本用法

1.1 常用方法

  • Add(int): 增加等待的计数器的值。每调用一次 Add 方法,计数器的值就会增加。
  • Done(): 减少等待的计数器的值。当一个 goroutine 完成任务时,它调用 Done 方法来通知 WaitGroup
  • Wait(): 阻塞当前 goroutine,直到计数器的值变为零,即所有 goroutine 都调用了 Done

1.2 示例代码

以下是一个使用 sync.WaitGroup 的基本示例:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup

    // 启动多个 goroutine
    for i := 1; i <= 3; i++ {
        wg.Add(1) // 增加计数器
        go func(n int) {
            defer wg.Done() // 在 goroutine 完成时减少计数器
            fmt.Printf("Goroutine %d is starting\n", n)
            time.Sleep(time.Second * 2) // 模拟工作
            fmt.Printf("Goroutine %d is done\n", n)
        }(i)
    }

    wg.Wait() // 阻塞主 goroutine,直到所有 goroutine 完成
    fmt.Println("All goroutines are done")
}

2. 工作原理

sync.WaitGroup 的内部实现基于计数器和条件变量。以下是 WaitGroup 的工作原理:

2.1 计数器

  • sync.WaitGroup 使用一个计数器来跟踪当前正在等待的 goroutine 数量。Add 方法增加计数器,Done 方法减少计数器,Wait 方法会阻塞直到计数器变为零。

2.2 条件变量

  • 内部使用了条件变量(sync.Cond)来实现阻塞和通知机制。当计数器不为零时,调用 Wait 方法的 goroutine 会被阻塞;当计数器减少到零时,条件变量会被通知,从而解除阻塞。

3. 注意事项和常见问题

3.1 确保调用 Done 的次数与 Add 相匹配

每个调用 Add 的 goroutine 都应该对应一个 Done 调用,否则 Wait 将会永久阻塞。

3.2 避免在 WaitGroup 计数器为负时调用 Wait

如果在 WaitGroup 计数器已经减为负值时调用 Wait,会导致程序挂起。确保所有的 AddDone 调用都匹配。

3.3 不要在 Wait 之前调用 Add

Add 方法必须在调用 Wait 之前调用。否则,如果 Wait 被调用时计数器已经为零,Wait 会立即返回,这可能会导致不正确的行为。

4. 高级用法和注意事项

4.1 使用 sync.WaitGroupcontext

在一些复杂的场景中,可能需要结合 sync.WaitGroupcontext.Context 来管理 goroutine 的生命周期和取消操作。

package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            select {
            case <-ctx.Done():
                fmt.Printf("Goroutine %d is canceled\n", n)
                return
            case <-time.After(time.Second * 2):
                fmt.Printf("Goroutine %d is done\n", n)
            }
        }(i)
    }

    // Simulate work in main goroutine
    time.Sleep(time.Second)
    cancel() // Cancel all goroutines

    wg.Wait() // Wait for all goroutines to finish
    fmt.Println("All goroutines are done")
}

在这个示例中,使用 context 来控制 goroutine 的取消,这可以与 sync.WaitGroup 结合使用来处理复杂的同步问题。

总结

sync.WaitGroup 是 Go 语言中用于同步等待多个 goroutine 完成的强大工具。它通过简单的计数器和条件变量机制,实现了高效的 goroutine 协调。正确使用 sync.WaitGroup 可以有效地管理并发任务,避免常见的同步问题。