插件窝 干货文章 Go语言sync.WaitGroup如何正确使用才能避免并发竞争?

Go语言sync.WaitGroup如何正确使用才能避免并发竞争?

WaitGroup 调用 goroutine Done 691    来源:    2025-03-15

在Go语言中,sync.WaitGroup 用于等待一组 goroutine 完成执行。为了避免并发竞争(race condition),需要正确使用 sync.WaitGroup。以下是一些关键点和最佳实践:

1. 正确初始化 WaitGroup

在使用 WaitGroup 之前,必须先初始化它。通常使用 var wg sync.WaitGroup 来声明一个 WaitGroup 变量。

2. 在启动 goroutine 之前调用 Add

在启动每个 goroutine 之前,应该调用 wg.Add(1) 来增加 WaitGroup 的计数器。这样可以确保 WaitGroup 知道有多少个 goroutine 需要等待。

wg.Add(1)
go func() {
    defer wg.Done()
    // 执行任务
}()

3. 在 goroutine 结束时调用 Done

在每个 goroutine 结束时,应该调用 wg.Done() 来减少 WaitGroup 的计数器。通常使用 defer wg.Done() 来确保即使 goroutine 发生 panic,Done 也会被调用。

go func() {
    defer wg.Done()
    // 执行任务
}()

4. 在主 goroutine 中调用 Wait

在主 goroutine 中调用 wg.Wait() 来等待所有 goroutine 完成。Wait 会阻塞,直到 WaitGroup 的计数器归零。

wg.Wait()

5. 避免在 goroutine 中调用 Add

不要在 goroutine 内部调用 wg.Add(1),因为这可能导致竞争条件。Add 应该在主 goroutine 中调用,确保在启动 goroutine 之前计数器已经增加。

6. 使用 sync.WaitGroup 的指针

如果 WaitGroup 需要在多个函数之间传递,应该使用指针传递 WaitGroup,而不是值传递。这样可以确保所有 goroutine 操作的是同一个 WaitGroup

func worker(wg *sync.WaitGroup) {
    defer wg.Done()
    // 执行任务
}

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go worker(&wg)
    wg.Wait()
}

7. 避免重复调用 Done

确保每个 Add 调用都有对应的 Done 调用,并且不要重复调用 Done,否则会导致 WaitGroup 的计数器变为负数,从而引发 panic。

8. 使用 sync.WaitGroupdefer 结合

在 goroutine 中使用 defer wg.Done() 是一个好习惯,因为它可以确保即使 goroutine 发生 panic,Done 也会被调用。

示例代码

以下是一个完整的示例,展示了如何正确使用 sync.WaitGroup

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d starting\n", id)
    // 模拟工作
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("All workers done")
}

总结

  • 在启动 goroutine 之前调用 Add
  • 在 goroutine 结束时调用 Done
  • 在主 goroutine 中调用 Wait
  • 使用 defer wg.Done() 确保 Done 被调用。
  • 避免在 goroutine 中调用 Add
  • 使用指针传递 WaitGroup

通过遵循这些最佳实践,可以避免并发竞争,并确保 sync.WaitGroup 的正确使用。