MST
星途 面试题库

面试题:Go中WaitGroup原理与优化

简述Go语言中WaitGroup的实现原理。如果在一个高并发场景下,有大量的并发任务使用WaitGroup进行同步等待,可能会出现什么性能问题,如何优化?请结合代码示例说明。
28.2万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

WaitGroup实现原理

  1. 结构定义WaitGroup内部包含一个计数器,用于记录等待的任务数量。其结构体类似如下:
type WaitGroup struct {
    noCopy noCopy
    state1 [3]uint32
}

state1数组用来存储计数器的值和一个信号量。 2. Add方法Add方法用于增加计数器的值,通常在启动新的并发任务前调用。它会根据传入的参数,原子地增加计数器的值。例如:

var wg sync.WaitGroup
wg.Add(5)
  1. Done方法Done方法用于减少计数器的值,通常在并发任务完成时调用。它实际上是调用了Add(-1),以原子方式减少计数器的值。
defer wg.Done()
  1. Wait方法Wait方法会阻塞当前协程,直到计数器的值变为0。它通过信号量机制来实现阻塞和唤醒。当计数器为0时,等待的协程会被唤醒继续执行。
wg.Wait()

高并发场景下可能出现的性能问题

  1. 大量等待协程的调度开销:当有大量并发任务使用WaitGroup时,Wait方法会阻塞大量协程。调度器需要频繁地切换上下文,导致较高的调度开销。
  2. 锁争用WaitGroup内部的计数器操作需要原子操作,这些操作涉及到锁。在高并发情况下,锁争用可能会成为性能瓶颈。

优化方法及代码示例

  1. 分批处理任务:将大量任务分成多个批次,每个批次使用独立的WaitGroup。这样可以减少每个WaitGroup等待的协程数量,降低调度开销。
package main

import (
    "fmt"
    "sync"
)

func main() {
    const numTasks = 1000
    const batchSize = 100
    var wgAll sync.WaitGroup
    wgAll.Add(numTasks / batchSize)

    for i := 0; i < numTasks; i += batchSize {
        var wg sync.WaitGroup
        wg.Add(batchSize)
        for j := 0; j < batchSize && i + j < numTasks; j++ {
            go func(taskNum int) {
                defer wg.Done()
                // 模拟任务执行
                fmt.Printf("Task %d is done\n", taskNum)
            }(i + j)
        }
        go func() {
            wg.Wait()
            wgAll.Done()
        }()
    }
    wgAll.Wait()
}
  1. 使用Channel进行同步:在某些场景下,可以使用Channel来替代WaitGroup进行同步,避免锁争用。
package main

import (
    "fmt"
)

func main() {
    const numTasks = 1000
    done := make(chan struct{}, numTasks)

    for i := 0; i < numTasks; i++ {
        go func(taskNum int) {
            // 模拟任务执行
            fmt.Printf("Task %d is done\n", taskNum)
            done <- struct{}{}
        }(i)
    }

    for i := 0; i < numTasks; i++ {
        <-done
    }
    close(done)
}

这种方式通过向Channel发送数据来表示任务完成,主线程通过接收Channel数据来等待所有任务完成,避免了WaitGroup内部的锁争用。