网站首页 > 文章精选 正文
在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 开发者处理并发同步的首选工具,适用于需要等待多个并发操作全部完成的场景。其设计简洁高效,但需注意计数器的正确管理。了解其计数器机制与使用原则,能够帮助我们写出更健壮、更优雅的并发代码。
猜你喜欢
- 2025-07-14 golang企业微信告警(企业微信告警推送)
- 2025-07-14 2.8 Go语言中的for循环,break和continue
- 2025-07-14 Go语言Context包:最常用包之一的使用指南
- 2025-07-14 Go语言零到一:动态数组——切片(go语言的切片)
- 2025-07-14 2025-06-26:转换数组。用go语言,给你一个整数数组 nums,它被视
- 2025-07-14 go sync.Pool简介(go system)
- 2025-07-14 2025-07-13:统计特殊子序列的数目。用go语言,给定一个只包含正
- 2025-07-14 Go语言数据库编程:GORM 的基本使用
- 2025-07-14 2025-06-28:长度可被 K 整除的子数组的最大元素和。用go语言,给
- 2025-07-14 Go语言零到一:初识变量(go语言示例)
- 最近发表
- 标签列表
-
- newcoder (56)
- 字符串的长度是指 (45)
- drawcontours()参数说明 (60)
- unsignedshortint (59)
- postman并发请求 (47)
- python列表删除 (50)
- 左程云什么水平 (56)
- 编程题 (64)
- postgresql默认端口 (66)
- 数据库的概念模型独立于 (48)
- 产生系统死锁的原因可能是由于 (51)
- 数据库中只存放视图的 (62)
- 在vi中退出不保存的命令是 (53)
- 哪个命令可以将普通用户转换成超级用户 (49)
- noscript标签的作用 (48)
- 联合利华网申 (49)
- swagger和postman (46)
- 结构化程序设计主要强调 (53)
- 172.1 (57)
- apipostwebsocket (47)
- 唯品会后台 (61)
- 简历助手 (56)
- offshow (61)
- mysql数据库面试题 (57)
- fmt.println (52)