程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

Go并发编程之WaitGroup(go语言 并发)

balukai 2025-07-14 14:50:37 文章精选 3 ℃

在Go编程中,往往主程序中并不知道goroutine执行结束的时间。假如希望等到goroutine执行结束之后再执行后面的逻辑,这个时候该怎么办?可以使用WaitGroup来解决。

WaitGroup 是标准库 sync 包中的一个并发同步工具。它通过计数器跟踪还未完成的工作 goroutine 的数量,等待一组 goroutine 全部执行完毕。实现对多个并发任务的集中管理,是构建可靠并发程序的核心工具,可以帮助开发者优雅地实现并发控制。本文将介绍WaitGroup的基本用法,使用场景及其常见错误和避坑指南。

1.WaitGroup 的核心机制

WaitGroup 的本质是一个带计数器的同步器,其核心通过三个方法实现协调:

Add(delta int):增加计数器值,需在启动 goroutine 前调用。

Done():减少计数器值,等价于Add(-1),通常用defer确保执行。

Wait():阻塞当前 goroutine,直至计数器归零。

WaitGroup工作原理图如下:

该图展示了 WaitGroup 的三个主要方法如何协同工作,WaitGroup 如何在并发环境中协调任务的启动、完成和等待过程。这种机制使得主程序能精准等待所有异步任务结束,避免因goroutine未完成而提前退出。

2.一个简单的经典示例

package main 
  
import (
    "fmt"
    "sync"
    "time"
)
  
func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 确保goroutine结束时减少计数器
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Duration(id) * time.Second) // 模拟耗时操作 
    fmt.Printf("Worker %d done\n", id)
}
  
func main() {
    var wg sync.WaitGroup 
    for i := 1; i <= 3; i++ {
        wg.Add(1) // 增加计数器
        id := i // 修正:使用局部变量避免竞争条件
        go worker(id, &wg) // 启动一个goroutine 
    }
    wg.Wait() // 等待所有goroutine完成 
    fmt.Println("All workers done")
}

执行结果:

示例的工作流程如下:

main主程序

启动 Worker 1: wg.Add(1) → go worker()

└─ Worker 1: defer wg.Done() → 任务完成后计数器-1

启动 Worker 2: wg.Add(1) → go worker()

└─ Worker 2: defer wg.Done() → 任务完成后计数器-1

启动 Worker 3: wg.Add(1) → go worker()

└─ Worker 3: defer wg.Done() → 任务完成后计数器-1

wg.Wait() → 阻塞直至计数器从3减为0 → 继续执行后续逻辑

其中,Add 和 Done方法内部使用原子操作来确保在多线程环境中计数值的准确性。在调用 Wait() 之前,确保所有的 Done() 调用都已完成,以避免由于内存同步问题导致的竞态条件。

3.WaigGroup的特点和使用场景

WaitGroup 的设计体现了 Go 语言 “最小接口,最大功能”,有以下几个特点:

  • 极简抽象:仅通过三个方法实现复杂的多任务同步
  • 轻量高效:基于原子操作实现,性能优于 channel 同步。
  • 并发安全:所有方法可安全并发调用。
  • 零值可用:无需初始化:var wg sync.WaitGroup 直接使用

通常,在以下几个使用场景可以使用:

1) 等待所有 goroutine 退出

2) 批量并发任务同步

3) 资源清理后退出

4) 分阶段任务同步

4.常见错误和避坑指南

错误1 计数器设置为负值

wg := sync.WaitGroup{}
wg.Add(5) // 正确:计数设置为5
wg.Add(-10) //错误:将-10作为参数调用Add,计数值被设置为0

避坑指南:WaitGroup的计数器的值必须大于等于0。在更改这个计数值的时候,WaitGroup会先做检查,如果计数值被设置为负数,就会导致panic。

错误2 Add位置不当

go func() {
    wg.Add(1) // 错误:Add在goroutine内部调用,可能晚于 wait执行
    defer wg.Done()
    // ...工作任务...
}()
wg.Wait() // 可能提前结束

避坑指南:Add必须在goroutine外部调用,确保主线程在Wait前计数器已完成增加。

错误3 忘记调用Done

wg.Add(2)
go func() {
    // 工作任务...
    // 忘记 wg.Done()!
}()
 
go func() {
    defer wg.Done() // 仅一个wg.Done()
}()
wg.Wait() // 死锁!

避坑指南:使用defer保证Done必然执行,尤其在复杂逻辑和错误处理分支中。

错误4 Wait未阻塞主线程

func Start() {
    var wg sync.WaitGroup
    wg.Add(1)
    go worker(&wg)
    // 函数返回,wg被回收!worker中的Done将操作无效内存!
}
 
func worker(wg *sync.WaitGroup) {
    defer wg.Done()
    // ...长时间工作...
}

避坑指南:确保WaitGroup生命周期覆盖所有goroutine。传递WaitGroup指针时,必须保证调用Wait的代码会等待所有关联goroutine结束。

错误5 WaitGroup未结束就重用

wg := sync.WaitGroup{}
wg.Add(1)
go func() {
     wg.Done() 
     wg.Add(1)  //如果跟wait并发执行,会panic
}()
wg.Wait()

避坑指南:WaitGroup不是设计来重用的。每次同步任务完成后应创建新实例。如需重用,必须确保所有旧计数已归零且无残留状态。否则会panic.

错误6 值传递非指针传递

//错误:值传递会创建WaitGroup副本,原计数器未改变
go worker(i, wg)
//正确:传递指针确保操作同一计数器
go worker(i, &wg)

避坑指南:始终通过指针传递 WaitGroup(&wg),避免值传递导致计数器副本.

安全使用WaitGroup的黄金法则如下:

  • Add前置:在启动goroutine前调用Add
  • Done保险:始终使用defer wg.Done()
  • 生命周期管理:确保WaitGroup存活至所有任务完成
  • 禁止重用:每个同步任务使用独立实例

大体可以概括为“提前Add、及时Done、正确Wait”的原则。

5.总结

并发工具的价值不在于复杂,而在于被正确使用。sync.WaitGroup 是 Go 并发编程中的基础工具,是Go 开发者处理并发同步的首选工具,适用于需要等待多个并发操作全部完成的场景。其设计简洁高效,但需注意计数器的正确管理。了解其计数器机制与使用原则,能够帮助我们写出更健壮、更优雅的并发代码。

Tags:

最近发表
标签列表