WaitGroup 等待组 - Golang并发编程面试题
WaitGroup是Go语言并发编程的重要同步原语,用于等待一组goroutine完成执行。本章深入解析WaitGroup的使用、原理和最佳实践。
📋 重点面试题
面试题 1:WaitGroup的基本用法和工作原理
难度级别:⭐⭐⭐
考察范围:并发同步/goroutine管理
技术标签:sync.WaitGroup Add Done Wait
问题分析
WaitGroup是Go并发编程的基础同步工具,面试官经常考查其正确使用方法和常见陷阱。
详细解答
1. WaitGroup基本概念
点击查看完整代码实现
点击查看完整代码实现
go
package main
import (
"fmt"
"sync"
"time"
)
// WaitGroup基本用法演示
func basicWaitGroupDemo() {
var wg sync.WaitGroup
// 启动3个goroutine
for i := 0; i < 3; i++ {
wg.Add(1) // 增加等待计数
go func(id int) {
defer wg.Done() // 完成时减少计数
fmt.Printf("Goroutine %d 开始工作\n", id)
time.Sleep(time.Duration(id) * time.Second)
fmt.Printf("Goroutine %d 完成工作\n", id)
}(i)
}
fmt.Println("等待所有goroutine完成...")
wg.Wait() // 阻塞直到计数变为0
fmt.Println("所有goroutine已完成")
}:::
2. WaitGroup内部结构原理
点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// WaitGroup的底层结构(简化版)
type WaitGroup struct {
noCopy noCopy
// state1包含计数器和等待者数量
state1 [3]uint32
}
// WaitGroup的状态布局
// 64位系统上:
// |-------- counter (32bit) -------|-------- waiter (32bit) -------|
// |-------------- sema (32bit) -------------|
func explainWaitGroupStructure() {
/*
WaitGroup使用了一个巧妙的设计:
1. counter: 跟踪还有多少个goroutine需要完成
2. waiter: 跟踪有多少个goroutine在等待
3. sema: 信号量,用于阻塞和唤醒等待的goroutine
Add(delta): 增加counter的值
Done(): 相当于Add(-1),减少counter的值
Wait(): 如果counter > 0,增加waiter计数并阻塞
*/
fmt.Println("WaitGroup内部使用信号量和原子操作实现同步")
}::: :::
3. WaitGroup的正确使用模式
点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// 正确的并发任务处理模式
func correctWaitGroupPattern() {
var wg sync.WaitGroup
tasks := []string{"task1", "task2", "task3", "task4"}
for _, task := range tasks {
wg.Add(1) // 在启动goroutine之前增加计数
go func(taskName string) {
defer wg.Done() // 使用defer确保Done一定会被调用
fmt.Printf("处理任务: %s\n", taskName)
// 模拟任务处理
time.Sleep(time.Duration(len(taskName)) * 100 * time.Millisecond)
fmt.Printf("任务 %s 完成\n", taskName)
}(task) // 避免闭包变量捕获问题
}
wg.Wait()
fmt.Println("所有任务处理完成")
}::: :::
面试题 2:WaitGroup常见错误和陷阱
难度级别:⭐⭐⭐⭐
考察范围:并发编程陷阱/调试技巧
技术标签:死锁 panic 竞态条件 闭包陷阱
问题分析
WaitGroup使用中有很多常见陷阱,理解这些陷阱有助于编写健壮的并发代码。
详细解答
1. 常见错误:Add和Done不匹配
点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// 错误示例:Add和Done不匹配导致死锁
func wrongAddDonePattern() {
var wg sync.WaitGroup
// 错误1:Add在goroutine内部调用
go func() {
wg.Add(1) // ❌ 错误:可能导致Wait()在Add()之前调用
defer wg.Done()
time.Sleep(1 * time.Second)
fmt.Println("任务完成")
}()
wg.Wait() // 可能在Add()之前就开始等待,导致死锁
}
// 正确示例:在启动goroutine之前调用Add
func correctAddPattern() {
var wg sync.WaitGroup
wg.Add(1) // ✅ 正确:在启动goroutine之前增加计数
go func() {
defer wg.Done()
time.Sleep(1 * time.Second)
fmt.Println("任务完成")
}()
wg.Wait()
}::: :::
2. 常见错误:WaitGroup复制
点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// WaitGroup不能复制的演示
func demonstrateNoCopy() {
var wg sync.WaitGroup
// ❌ 错误:传递WaitGroup的值会复制
// 复制的WaitGroup状态独立,Wait()无法等待原始的goroutine
go func(wg sync.WaitGroup) { // 错误:按值传递
defer wg.Done()
time.Sleep(1 * time.Second)
}(wg)
// ✅ 正确:传递指针
wg.Add(1)
go func(wg *sync.WaitGroup) {
defer wg.Done()
time.Sleep(1 * time.Second)
}(&wg)
wg.Wait()
}
// 使用结构体包含WaitGroup的正确方式
type TaskManager struct {
wg sync.WaitGroup
}
func (tm *TaskManager) AddTask() {
tm.wg.Add(1)
}
func (tm *TaskManager) TaskDone() {
tm.wg.Done()
}
func (tm *TaskManager) WaitAll() {
tm.wg.Wait()
}::: :::
3. 常见错误:闭包变量捕获
点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// 闭包变量捕获陷阱
func closureTrap() {
var wg sync.WaitGroup
// ❌ 错误:所有goroutine共享同一个变量i
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Printf("处理任务 %d\n", i) // i的值可能都是3
}()
}
// ✅ 正确:通过参数传递值
for i := 0; i < 3; i++ {
wg.Add(1)
go func(taskId int) {
defer wg.Done()
fmt.Printf("处理任务 %d\n", taskId)
}(i)
}
// ✅ 正确:在循环内声明新变量
for i := 0; i < 3; i++ {
wg.Add(1)
i := i // 声明新的局部变量
go func() {
defer wg.Done()
fmt.Printf("处理任务 %d\n", i)
}()
}
wg.Wait()
}::: :::
4. 常见错误:负计数导致panic
点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
func negativeCounterPanic() {
var wg sync.WaitGroup
// 场景1:Done()调用次数超过Add()
wg.Add(2)
go func() {
defer wg.Done()
time.Sleep(100 * time.Millisecond)
}()
go func() {
defer wg.Done()
defer wg.Done() // ❌ 错误:多调用了一次Done(),会panic
time.Sleep(100 * time.Millisecond)
}()
wg.Wait()
}
// 安全的Done调用模式
func safeDonePattern() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("捕获到panic: %v\n", r)
}
wg.Done() // 确保Done只调用一次
}()
// 可能出现panic的代码
// panic("模拟错误")
}()
wg.Wait()
}::: :::
面试题 3:WaitGroup的高级应用场景
难度级别:⭐⭐⭐⭐
考察范围:实战应用/并发模式
技术标签:worker池 限流 超时控制 错误收集
问题分析
在实际项目中,WaitGroup经常与其他并发原语结合使用,实现复杂的并发控制模式。
详细解答
1. Worker Pool模式
点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
import (
"context"
"fmt"
"sync"
"time"
)
// 工作任务定义
type Task struct {
ID int
Data string
}
// Worker Pool使用WaitGroup
func workerPoolWithWaitGroup() {
const numWorkers = 3
const numTasks = 10
tasks := make(chan Task, numTasks)
var wg sync.WaitGroup
// 启动worker goroutine
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
worker(workerID, tasks)
}(i)
}
// 发送任务
for i := 0; i < numTasks; i++ {
tasks <- Task{ID: i, Data: fmt.Sprintf("task-%d", i)}
}
close(tasks) // 关闭任务通道,worker会自动退出
wg.Wait() // 等待所有worker完成
fmt.Println("所有任务处理完成")
}
func worker(id int, tasks <-chan Task) {
for task := range tasks {
fmt.Printf("Worker %d 处理任务 %d: %s\n", id, task.ID, task.Data)
time.Sleep(200 * time.Millisecond) // 模拟处理时间
fmt.Printf("Worker %d 完成任务 %d\n", id, task.ID)
}
fmt.Printf("Worker %d 退出\n", id)
}::: :::
2. 带超时控制的WaitGroup
点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// 实现带超时的WaitGroup
func waitGroupWithTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
return true // 正常完成
case <-time.After(timeout):
return false // 超时
}
}
func demonstrateTimeoutWaitGroup() {
var wg sync.WaitGroup
// 启动一个耗时的任务
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(3 * time.Second) // 模拟长时间任务
fmt.Println("长时间任务完成")
}()
// 等待任务完成,但设置2秒超时
if waitGroupWithTimeout(&wg, 2*time.Second) {
fmt.Println("所有任务在超时前完成")
} else {
fmt.Println("任务未在指定时间内完成")
}
}::: :::
3. 错误收集模式
点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
import (
"errors"
"sync"
)
// 错误收集器
type ErrorCollector struct {
mu sync.Mutex
errors []error
}
func (ec *ErrorCollector) AddError(err error) {
if err != nil {
ec.mu.Lock()
ec.errors = append(ec.errors, err)
ec.mu.Unlock()
}
}
func (ec *ErrorCollector) GetErrors() []error {
ec.mu.Lock()
defer ec.mu.Unlock()
return append([]error(nil), ec.errors...) // 返回副本
}
// 并发处理并收集错误
func concurrentProcessingWithErrorCollection() {
var wg sync.WaitGroup
collector := &ErrorCollector{}
tasks := []int{1, 2, 3, 4, 5}
for _, task := range tasks {
wg.Add(1)
go func(taskID int) {
defer wg.Done()
if err := processTask(taskID); err != nil {
collector.AddError(fmt.Errorf("任务 %d 失败: %w", taskID, err))
}
}(task)
}
wg.Wait()
// 处理收集到的错误
if errs := collector.GetErrors(); len(errs) > 0 {
fmt.Printf("处理过程中发生了 %d 个错误:\n", len(errs))
for _, err := range errs {
fmt.Printf(" - %v\n", err)
}
} else {
fmt.Println("所有任务都成功完成")
}
}
func processTask(taskID int) error {
time.Sleep(100 * time.Millisecond)
// 模拟某些任务失败
if taskID%3 == 0 {
return errors.New("模拟的处理错误")
}
fmt.Printf("任务 %d 完成\n", taskID)
return nil
}::: :::
4. 分批处理模式
点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// 分批处理大量任务
func batchProcessingWithWaitGroup() {
const batchSize = 5
const totalTasks = 23
allTasks := make([]Task, totalTasks)
for i := 0; i < totalTasks; i++ {
allTasks[i] = Task{ID: i, Data: fmt.Sprintf("batch-task-%d", i)}
}
// 按批处理任务
for start := 0; start < len(allTasks); start += batchSize {
end := start + batchSize
if end > len(allTasks) {
end = len(allTasks)
}
batch := allTasks[start:end]
fmt.Printf("\n处理第 %d 批任务 (任务 %d-%d)\n", start/batchSize+1, start, end-1)
var wg sync.WaitGroup
for _, task := range batch {
wg.Add(1)
go func(t Task) {
defer wg.Done()
processBatchTask(t)
}(task)
}
wg.Wait()
fmt.Printf("第 %d 批任务完成\n", start/batchSize+1)
}
fmt.Println("所有批次处理完成")
}
func processBatchTask(task Task) {
fmt.Printf(" 处理任务 %d: %s\n", task.ID, task.Data)
time.Sleep(50 * time.Millisecond)
}::: :::
面试题 4:WaitGroup性能优化和最佳实践
难度级别:⭐⭐⭐⭐
考察范围:性能优化/生产实践
技术标签:性能优化 内存对齐 false sharing best practices
问题分析
在高并发场景下,WaitGroup的使用方式会影响性能,了解优化技巧很重要。
详细解答
1. 避免频繁的Add/Done调用
点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// 低效的方式:频繁调用Add
func inefficientWaitGroup() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1) // 每次循环都调用Add,可能产生竞争
go func(id int) {
defer wg.Done()
// 快速任务
fmt.Printf("快速任务 %d\n", id)
}(i)
}
wg.Wait()
}
// 高效的方式:批量Add
func efficientWaitGroup() {
var wg sync.WaitGroup
taskCount := 1000
wg.Add(taskCount) // 一次性添加所有计数
for i := 0; i < taskCount; i++ {
go func(id int) {
defer wg.Done()
fmt.Printf("快速任务 %d\n", id)
}(i)
}
wg.Wait()
}::: :::
2. 结合Context使用
点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// WaitGroup与Context结合使用
func waitGroupWithContext(ctx context.Context) {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
select {
case <-ctx.Done():
fmt.Printf("任务 %d 被取消: %v\n", id, ctx.Err())
return
case <-time.After(time.Duration(id) * time.Second):
fmt.Printf("任务 %d 正常完成\n", id)
}
}(i)
}
// 启动一个goroutine来等待WaitGroup
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
// 等待完成或取消
select {
case <-done:
fmt.Println("所有任务完成")
case <-ctx.Done():
fmt.Printf("操作被取消: %v\n", ctx.Err())
}
}
func demonstrateContextWaitGroup() {
// 创建可取消的context
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
waitGroupWithContext(ctx)
}::: :::
3. WaitGroup最佳实践总结
点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// WaitGroup最佳实践示例
type SafeWaitGroupManager struct {
wg sync.WaitGroup
mu sync.RWMutex
}
func (swg *SafeWaitGroupManager) Add(delta int) {
swg.mu.Lock()
defer swg.mu.Unlock()
swg.wg.Add(delta)
}
func (swg *SafeWaitGroupManager) Done() {
swg.mu.RLock()
defer swg.mu.RUnlock()
swg.wg.Done()
}
func (swg *SafeWaitGroupManager) Wait() {
swg.mu.RLock()
defer swg.mu.RUnlock()
swg.wg.Wait()
}
// 最佳实践要点
func waitGroupBestPractices() {
fmt.Println("WaitGroup最佳实践:")
fmt.Println("1. 在启动goroutine之前调用Add()")
fmt.Println("2. 使用defer确保Done()被调用")
fmt.Println("3. 不要复制WaitGroup,通过指针传递")
fmt.Println("4. Add和Done的调用必须配对")
fmt.Println("5. 避免在goroutine内部调用Add()")
fmt.Println("6. 考虑使用Context进行取消控制")
fmt.Println("7. 在高并发场景下批量调用Add()")
fmt.Println("8. 结合其他同步原语实现复杂的并发控制")
}::: :::
🎯 核心知识点总结
WaitGroup基础要点
- 基本操作: Add()增加计数,Done()减少计数,Wait()等待计数归零
- 内部实现: 使用原子操作和信号量实现高效同步
- 生命周期: 计数器从0开始,通过Add增加,Done减少,Wait在计数为0时返回
- 并发安全: WaitGroup的所有方法都是并发安全的
常见陷阱要点
- Add/Done不匹配: 会导致死锁或panic
- WaitGroup复制: 按值传递会导致状态不同步
- 闭包变量捕获: 循环中启动goroutine要注意变量作用域
- 负计数: Done()调用过多会导致panic
高级应用要点
- Worker Pool: 结合channel实现工作池模式
- 超时控制: 结合select和timer实现超时等待
- 错误收集: 并发任务的错误统一收集和处理
- 批量处理: 分批处理大量任务,控制并发度
性能优化要点
- 批量Add: 避免频繁的Add调用减少竞争
- Context结合: 支持取消和超时控制
- 内存对齐: 注意WaitGroup在结构体中的位置
- 适度并发: 控制goroutine数量避免过度并发
🔍 面试准备建议
- 掌握原理: 理解WaitGroup的内部实现和工作机制
- 识别陷阱: 熟悉常见错误模式和解决方案
- 实战练习: 通过编写并发程序加深理解
- 性能考虑: 了解在高并发场景下的性能优化技巧
