在Golang的并发编程中,goroutine是轻量级线程,能够高效处理并行任务,但默认情况下主协程不会等待子协程完成就会退出程序,这时候就需要同步机制来保证所有子协程的任务都执行完毕。WaitGroup是sync包提供的专门用于等待一组协程完成的工具,使用起来简单高效。
WaitGroup核心方法
WaitGroup提供了三个核心方法来控制协程同步,分别是:
- Add(delta int):设置需要等待的协程数量,delta为正整数表示增加等待数量,为负整数表示减少等待数量
- Done():每个协程执行完成后调用,等价于Add(-1),减少等待计数
- Wait():阻塞当前协程,直到等待计数归零,即所有需要等待的协程都执行完成
基本使用流程
使用WaitGroup同步协程的标准流程分为三步:
- 声明一个sync.WaitGroup类型的变量
- 在启动每个子协程前调用WaitGroup的Add方法增加等待计数
- 在子协程内部执行完任务后调用Done方法,主协程中调用Wait方法阻塞等待所有子协程完成
完整代码示例
下面的代码演示了启动3个协程并行处理任务,主协程等待所有协程完成后再输出的场景:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
// 声明WaitGroup变量
var wg sync.WaitGroup
// 设置需要等待的协程数量为3
wg.Add(3)
// 启动第一个协程
go func(id int) {
defer wg.Done() // 协程执行完成后减少等待计数
time.Sleep(1 * time.Second)
fmt.Printf("协程%d执行完成n", id)
}(1)
// 启动第二个协程
go func(id int) {
defer wg.Done()
time.Sleep(2 * time.Second)
fmt.Printf("协程%d执行完成n", id)
}(2)
// 启动第三个协程
go func(id int) {
defer wg.Done()
time.Sleep(1 * time.Second)
fmt.Printf("协程%d执行完成n", id)
}(3)
// 主协程阻塞等待所有子协程完成
wg.Wait()
fmt.Println("所有协程执行完毕,主协程继续运行")
}
注意事项
使用WaitGroup时需要注意以下几点,避免出现逻辑错误:
- Add方法的调用必须在启动协程之前完成,否则可能出现协程已经执行完成但Add还没调用,导致Wait提前返回的问题
- 不要在多个协程中并发调用Add方法,避免计数出现不一致的情况,建议在启动协程的循环中统一调用Add
- Done方法最好通过defer关键字调用,确保即使协程内部出现panic也能正确减少等待计数,避免主协程永久阻塞
- WaitGroup变量不需要初始化,零值就是可用的状态,不需要额外赋值
常见错误场景
如果忘记调用Wait方法,主协程会直接退出,子协程的任务可能无法执行完成,比如下面的错误示例:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("子协程执行")
}()
// 没有调用wg.Wait(),主协程直接退出,子协程可能不会打印内容
fmt.Println("主协程退出")
}
这种场景下程序运行时很可能不会输出"子协程执行",因为主协程执行完打印后就直接结束了,子协程还没得到调度执行的机会。
适用场景
WaitGroup适合需要等待一组同类协程全部完成的场景,比如批量处理任务、并发请求多个接口后汇总结果等。如果是需要等待单个协程完成,或者需要传递结果的场景,可以结合channel或者context使用,但WaitGroup在纯同步场景下是最高效的选择。