
sync.WaitGroup概述
在Go语言中,当我们需要启动多个Go协程并发执行任务,并且主Go协程需要等待所有这些任务完成后才能继续后续操作时,sync.WaitGroup提供了一种简洁高效的解决方案。它内部维护一个计数器:
- Add(delta int):将计数器增加delta。通常在启动新的Go协程之前调用,以告知WaitGroup有多少个任务需要等待。
- Done():将计数器减一。通常在Go协程完成其任务时调用。
- Wait():阻塞调用者,直到计数器归零。这意味着所有通过Add增加的任务都已通过Done完成。
sync.WaitGroup的工作原理与使用示例
为了更好地理解WaitGroup的工作方式,我们来看一个经典的例子:启动多个工作协程执行模拟任务,并等待它们全部完成。
package main
import (
"fmt"
"sync"
"time"
)
// worker 函数模拟一个耗时任务,并在完成后通知 WaitGroup
func worker(id int, wg *sync.WaitGroup) {
// 确保在 worker 协程退出前调用 wg.Done(),无论是正常完成还是发生 panic
defer wg.Done()
fmt.Printf("Worker %d: 任务开始...\n", id)
time.Sleep(time.Duration(id) * time.Second) // 模拟耗时操作
fmt.Printf("Worker %d: 任务完成。\n", id)
}
func main() {
var wg sync.WaitGroup // 声明一个 WaitGroup 实例
numWorkers := 3 // 我们将启动3个工作协程
fmt.Println("主协程: 启动工作协程...")
// 循环启动工作协程
for i := 1; i <= numWorkers; i++ {
wg.Add(1) // 每次启动一个新协程,计数器加1
go worker(i, &wg) // 启动协程,并传递 WaitGroup 的指针
}
fmt.Println("主协程: 等待所有工作协程完成...")
wg.Wait() // 阻塞主协程,直到 WaitGroup 计数器归零
fmt.Println("主协程: 所有工作协程已完成。程序退出。")
}
代码解析:
- 在main函数中,我们首先声明了一个sync.WaitGroup变量wg。
- 在循环中,每次启动一个worker协程之前,我们都调用wg.Add(1),这会将WaitGroup的内部计数器增加1。这表示我们期望有一个新的Go协程将要完成任务。
- worker函数接收一个id和一个*sync.WaitGroup指针。在worker函数内部,我们使用defer wg.Done()。defer语句确保了无论worker函数如何结束(正常返回或发生panic),wg.Done()都会被调用,从而将WaitGroup的计数器减1。
- 在main函数的最后,wg.Wait()被调用。这个方法会阻塞main协程的执行,直到WaitGroup的内部计数器变为0。只有当所有通过Add增加的Go协程都调用了Done()后,Wait()才会解除阻塞,main协程才能继续执行后续的代码。
运行上述代码,您会看到工作协程并发执行,并且主协程会等待所有工作协程完成后才打印最终的退出信息。
关键概念区分:WaitGroup与Mutex
值得注意的是,sync.WaitGroup的主要目的是等待一组Go协程的完成,它关注的是同步任务的生命周期。它不提供任何机制来保护共享资源的并发访问。
PHP网络编程技术详解由浅入深,全面、系统地介绍了PHP开发技术,并提供了大量实例,供读者实战演练。另外,笔者专门为本书录制了相应的配套教学视频,以帮助读者更好地学习本书内容。这些视频和书中的实例源代码一起收录于配书光盘中。本书共分4篇。第1篇是PHP准备篇,介绍了PHP的优势、开发环境及安装;第2篇是PHP基础篇,介绍了PHP中的常量与变量、运算符与表达式、流程控制以及函数;第3篇是进阶篇,介绍
与此相对,sync.Mutex(互斥锁)是用于保护共享资源,确保在任何给定时刻只有一个Go协程可以访问该资源,从而避免数据竞争。
在某些场景下,两者可能会被混淆。例如,如果一个问题是关于如何防止多个Go协程同时修改一个变量,那么应该使用sync.Mutex(或sync.RWMutex、sync.Atomic等),而不是sync.WaitGroup。WaitGroup无法解决数据竞争问题,它仅仅是用来协调Go协程的启动与结束。
使用注意事项
- Add的调用时机: 务必在启动新的Go协程之前调用wg.Add(1)。如果在Go协程启动之后再调用Add,可能会导致Wait()在计数器尚未正确增加之前就被调用,从而提前解除阻塞。
- Done的调用时机与defer: 推荐在每个工作Go协程的入口处使用defer wg.Done()。这样可以保证无论Go协程是正常完成还是因为运行时错误(panic)而退出,Done()都会被调用,避免主协程永远等待。
- 计数器值: WaitGroup的计数器不能为负值。如果Done()的调用次数超过了Add()的次数,程序会发生panic。
- 复用WaitGroup: 一个WaitGroup实例可以在完成一次等待循环后被复用,但必须确保在下一次使用前,其计数器已归零。通常情况下,为了代码清晰和避免潜在错误,每次需要等待一组新的Go协程时,声明一个新的WaitGroup实例会更安全。
总结
sync.WaitGroup是Go语言中实现Go协程同步的强大工具,特别适用于“扇出-扇入”(fan-out/fan-in)模式,即启动多个并发任务,然后等待所有任务完成的场景。通过熟练掌握Add、Done和Wait三个方法,开发者可以有效地管理并发Go协程的生命周期,确保程序的正确执行流程。理解其与sync.Mutex等其他同步原语的区别至关重要,以在不同的并发问题中选择最合适的工具。










