「Go语言面试题」09 - 什么是Go中的WaitGroup?如何使用它来同步Goroutine?


什么是WaitGroup?

在Go语言中,sync.WaitGroup是一个并发控制原语,用于等待一组Goroutine完成执行。它提供了一种简单而强大的方式来协调多个并发任务,确保主程序在所有Goroutine完成后再继续执行。

var wg sync.WaitGroup // WaitGroup声明

WaitGroup的核心方法

1. Add(delta int)

// 增加等待的Goroutine数量
wg.Add(1)  // 增加1个
wg.Add(3)  // 增加3个

2. Done()

// 表示一个Goroutine已完成(相当于Add(-1))
defer wg.Done() // 推荐使用defer确保调用

3. Wait()

// 阻塞当前Goroutine,直到所有任务完成
wg.Wait()

基础使用模式

func main() {
    var wg sync.WaitGroup
    
    for i := 0; i < 5; i++ {
        wg.Add(1) // 增加计数
        
        go func(id int) {
            defer wg.Done() // 任务完成时减少计数
            fmt.Printf("Goroutine %d 开始\n", id)
            time.Sleep(time.Second)
            fmt.Printf("Goroutine %d 结束\n", id)
        }(i)
    }
    
    fmt.Println("等待所有Goroutine完成...")
    wg.Wait() // 阻塞直到所有Done()被调用
    fmt.Println("所有任务完成!")
}

WaitGroup的四大核心特性

1. 轻量级同步机制

  • 基于原子操作实现
  • 无锁设计,高性能
  • 零值可用(无需初始化)

2. 阻塞等待机制

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    
    go func() {
        time.Sleep(time.Second)
        wg.Done()
    }()
    
    start := time.Now()
    wg.Wait() // 阻塞约1秒
    fmt.Println("等待时间:", time.Since(start))
}

3. 动态任务管理

func main() {
    var wg sync.WaitGroup
    
    // 动态添加任务
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go worker(&wg, i)
        
        if i == 1 {
            // 中途添加额外任务
            wg.Add(1)
            go worker(&wg, 10)
        }
    }
    
    wg.Wait()
}

4. 组合使用模式

func main() {
    var wg sync.WaitGroup
    ch := make(chan int)
    
    // 生产者
    wg.Add(1)
    go func() {
        defer wg.Done()
        for i := 0; i < 5; i++ {
            ch <- i
        }
        close(ch)
    }()
    
    // 消费者
    wg.Add(1)
    go func() {
        defer wg.Done()
        for num := range ch {
            fmt.Println("接收:", num)
        }
    }()
    
    wg.Wait()
}

五大高级使用场景

1. 批量任务处理

func processImages(images []string) {
    var wg sync.WaitGroup
    results := make(chan string, len(images))
    
    for _, img := range images {
        wg.Add(1)
        go func(imagePath string) {
            defer wg.Done()
            // 模拟图像处理
            result := resizeImage(imagePath)
            results <- result
        }(img)
    }
    
    // 等待所有任务完成
    go func() {
        wg.Wait()
        close(results) // 所有任务完成后关闭通道
    }()
    
    // 收集结果
    for res := range results {
        fmt.Println("处理结果:", res)
    }
}

2. 服务优雅关闭

func (s *Server) Shutdown() {
    var wg sync.WaitGroup
    
    // 关闭监听器
    wg.Add(1)
    go func() {
        defer wg.Done()
        s.listener.Close()
    }()
    
    // 关闭数据库连接
    wg.Add(1)
    go func() {
        defer wg.Done()
        s.db.Close()
    }()
    
    // 关闭缓存连接
    wg.Add(1)
    go func() {
        defer wg.Done()
        s.cache.Close()
    }()
    
    // 等待所有资源关闭
    wg.Wait()
    fmt.Println("服务优雅关闭完成")
}

3. 并发限制器

func limitedConcurrency(tasks []func(), maxConcurrent int) {
    var wg sync.WaitGroup
    sem := make(chan struct{}, maxConcurrent) // 信号量
    
    for _, task := range tasks {
        wg.Add(1)
        sem <- struct{}{} // 获取信号量
        
        go func(t func()) {
            defer func() {
                <-sem // 释放信号量
                wg.Done()
            }()
            t() // 执行任务
        }(task)
    }
    
    wg.Wait()
}

WaitGroup的四大常见陷阱

1. 计数器为负

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    wg.Done()
    wg.Done() // panic: sync: negative WaitGroup counter
}

解决方案

  • 确保Done()调用次数不超过Add()调用
  • 使用defer调用Done()

2. 过早调用Wait()

func main() {
    var wg sync.WaitGroup
    
    wg.Add(1)
    go func() {
        time.Sleep(time.Second)
        wg.Done()
    }()
    
    wg.Wait() // 正确等待
    
    wg.Add(1) // 错误:在Wait()后再次使用
    // ...
}

解决方案

  • 避免在Wait()后重用WaitGroup
  • 需要多次使用时创建新的WaitGroup

3. 忘记调用Done()

func main() {
    var wg sync.WaitGroup
    
    wg.Add(1)
    go func() {
        // 忘记调用wg.Done()
    }()
    
    wg.Wait() // 永久阻塞
}

解决方案

  • 使用defer确保Done()被调用
  • 结合recover防止panic导致Done()未调用
go func() {
    defer func() {
        if r := recover(); r != nil {
            // 处理panic
        }
        wg.Done()
    }()
    // 业务代码...
}()

4. 值传递导致的副本问题

func main() {
    var wg sync.WaitGroup
    
    wg.Add(1)
    go worker(wg) // 值传递创建副本
    
    wg.Wait() // 不会等待副本
}

func worker(wg sync.WaitGroup) {
    defer wg.Done()
    // ...
}

解决方案

  • 始终使用指针传递WaitGroup
  • 使用闭包捕获WaitGroup引用
// 正确做法1:指针传递
go worker(&wg)

// 正确做法2:闭包捕获
go func() {
    defer wg.Done()
    // ...
}()

性能优化技巧

1. 批量Add减少调用次数

// 低效:多次调用Add
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go worker(&wg)
}

// 高效:单次调用Add
wg.Add(1000)
for i := 0; i < 1000; i++ {
    go worker(&wg)
}

2. 避免在热点路径使用

// 不推荐:在频繁调用的函数中使用
func processItem(item Item, wg *sync.WaitGroup) {
    defer wg.Done()
    // ...
}

// 推荐:使用其他同步机制(如channel)
results := make(chan Result)
go processItems(items, results)

3. 组合使用sync.Pool

var wgPool = sync.Pool{
    New: func() interface{} { return new(sync.WaitGroup) },
}

func getWaitGroup() *sync.WaitGroup {
    return wgPool.Get().(*sync.WaitGroup)
}

func putWaitGroup(wg *sync.WaitGroup) {
    // 重置状态
    *wg = sync.WaitGroup{}
    wgPool.Put(wg)
}

WaitGroup与其他同步原语的对比

特性 WaitGroup Channel sync.Mutex
主要用途 等待任务组完成 数据传递/信号 临界区保护
阻塞机制 Wait()阻塞 发送/接收阻塞 Lock()阻塞
并发安全
复用性 有限
性能 极高
适用场景 任务同步 数据流控制 资源共享

最佳实践总结

  1. 指针传递:始终使用指针传递WaitGroup

    go worker(&wg)
    
  2. defer Done:使用defer确保Done()被调用

    defer wg.Done()
    
  3. 提前Add:在启动Goroutine前调用Add()

    wg.Add(1)
    go worker(&wg)
    
  4. 避免重用:每次任务组使用新的WaitGroup

    func processBatch(batch []Item) {
        var wg sync.WaitGroup
        // ...
    }
    
  5. 组合超时:与select结合实现超时控制

    select {
    case <-done:
    case <-time.After(timeout):
    }
    
  6. 错误处理:结合recover防止panic导致阻塞

    defer func() {
        if r := recover() { /* 处理 */ }
        wg.Done()
    }()
    

结论

Go的WaitGroup是并发编程中不可或缺的工具,它提供了:

  1. 简洁的任务组同步机制
  2. 高性能的并发控制能力
  3. 可靠的任务完成保证
  4. 灵活的多Goroutine协调方案

掌握WaitGroup的正确使用方法和最佳实践,能够帮助开发者构建出更健壮、更高效的并发程序。无论是简单的任务等待,还是复杂的分布式系统协调,WaitGroup都是Go并发工具箱中的核心组件。

wx

关注公众号

©2017-2023 鲁ICP备17023316号-1 Powered by Hugo