面试题答案
一键面试WaitGroup实现原理
- 结构定义:
WaitGroup
内部包含一个计数器,用于记录等待的任务数量。其结构体类似如下:
type WaitGroup struct {
noCopy noCopy
state1 [3]uint32
}
state1
数组用来存储计数器的值和一个信号量。
2. Add方法:Add
方法用于增加计数器的值,通常在启动新的并发任务前调用。它会根据传入的参数,原子地增加计数器的值。例如:
var wg sync.WaitGroup
wg.Add(5)
- Done方法:
Done
方法用于减少计数器的值,通常在并发任务完成时调用。它实际上是调用了Add(-1)
,以原子方式减少计数器的值。
defer wg.Done()
- Wait方法:
Wait
方法会阻塞当前协程,直到计数器的值变为0。它通过信号量机制来实现阻塞和唤醒。当计数器为0时,等待的协程会被唤醒继续执行。
wg.Wait()
高并发场景下可能出现的性能问题
- 大量等待协程的调度开销:当有大量并发任务使用
WaitGroup
时,Wait
方法会阻塞大量协程。调度器需要频繁地切换上下文,导致较高的调度开销。 - 锁争用:
WaitGroup
内部的计数器操作需要原子操作,这些操作涉及到锁。在高并发情况下,锁争用可能会成为性能瓶颈。
优化方法及代码示例
- 分批处理任务:将大量任务分成多个批次,每个批次使用独立的
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()
}
- 使用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
内部的锁争用。