Go并发双雄:WaitGroup与Channel的抉择与协作
在Go语言的并发编程中,等待Goroutine完成任务是一个高频需求。开发者往往面临一个选择:是使用sync.WaitGroup,还是使用Channel?虽然两者都能实现"等待"的效果,但它们的设计初衷、底层机制以及适用场景却大相径庭。
本文将深入剖析这两者的区别,帮助你根据实际业务场景做出最优选择。
WaitGroup:精准的计数器
sync.WaitGroup本质上是一个原子计数器。它的职责非常单一且明确:等待一组Goroutine全部执行完毕。
核心机制:
- Add(n):在启动Goroutine之前,增加计数。
- Done() :在Goroutine结束时(通常使用
defer),减少计数。 - Wait():阻塞当前Goroutine,直到计数归零。
适用场景:
- 纯粹的同步等待:你只关心任务"做完了没有",而不需要关心任务"返回了什么"。
- 批量任务处理:例如启动10个协程处理图片,主程序需要等所有图片处理完才能进行下一步打包。
- 高性能要求:WaitGroup基于原子操作实现,内存开销极小,性能极高。
代码示例:
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Worker %d 完成\n", id)
}(i)
}
wg.Wait() // 阻塞直到所有Worker调用Done
fmt.Println("所有任务结束")
Channel:灵活的通信管道
Channel不仅仅是同步工具,更是通信工具。在等待Goroutine完成的场景下,Channel通常通过"发送完成信号"或"传递结果"来实现同步。
核心机制:
- 无缓冲Channel:发送和接收操作会互相阻塞,形成"握手"同步。
- 带缓冲Channel:发送操作在缓冲区满之前不会阻塞,适合收集多个完成信号。
- 关闭Channel :通过
close(ch)广播退出信号,接收方通过range或ok检查感知结束。
适用场景:
- 需要获取结果:不仅要等任务完成,还要拿到任务的处理结果(如API响应数据)。
- 复杂流程控制 :需要实现超时控制(配合
time.After)、取消信号(context)或多路复用(select)。 - 解耦生产与消费:任务提交者和执行者不需要知道彼此的存在。
代码示例(收集结果):
results := make(chan int, 3)
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
results <- id * 2 // 发送结果
}(i)
}
// 另起协程等待所有任务完成后关闭通道
go func() {
wg.Wait()
close(results)
}()
// 接收结果
for res := range results {
fmt.Printf("收到结果: %d\n", res)
}
深度对比:何时使用哪一个?
| 维度 | WaitGroup | Channel |
|---|---|---|
| 核心语义 | 计数同步:等待N个任务结束 | 通信同步:传递数据或信号 |
| 数据传递 | 不支持(需配合外部变量,不安全) | 支持(天然线程安全) |
| 灵活性 | 低(仅支持等待完成) | 高(支持超时、取消、优先级) |
| 性能开销 | 极低(原子操作) | 中等(涉及内存分配和调度) |
| 典型模式 | 批量并发、初始化等待 | 生产者-消费者、结果收集 |
关键区别点:
- 死锁风险 :WaitGroup如果忘记调用
Done()会导致永久阻塞;Channel如果发送方不关闭或接收方不读取,也会导致死锁。但Channel可以通过select和context更容易地打破死锁(如超时退出)。 - 关闭规则:WaitGroup没有"关闭"概念,计数归零即释放;Channel必须遵循"谁发送谁关闭"的原则,多协程同时关闭会导致Panic。
最佳实践:强强联合
在实际的高级并发模式中,WaitGroup和Channel往往是协作关系,而非互斥关系。
经典组合模式:
- WaitGroup负责生命周期:确保所有Worker都处理完任务。
- Channel负责数据传输:Worker将结果发送到Channel。
- 独立的关闭协程 :启动一个专门的Goroutine,它调用
wg.Wait(),然后close(ch)。这样接收端就能安全地使用range遍历结果,而不用担心Channel过早关闭导致的数据丢失。
总结: 如果你只需要一个简单的"路障",等大家到齐了再出发,用WaitGroup;如果你需要构建一条"流水线",既要等大家干完,又要接收大家生产的产品,甚至还要控制流水线的开关,那么Channel(通常配合WaitGroup)是你的不二之选。