面试题答案
一键面试WaitGroup数据结构
在Go源码(src/sync/waitgroup.go
)中,WaitGroup
的数据结构定义如下:
type WaitGroup struct {
noCopy noCopy
state1 [3]uint32
}
noCopy
字段:它是一个空结构体,使用vendor/golang.org/x/tools/go/analysis/passes/nocopy
包来防止WaitGroup
被复制,因为复制WaitGroup
会导致未定义行为。state1
字段:它是一个包含3个uint32
的数组,用来保存状态信息。前两个uint32
组合成一个64位的值,用于表示等待的计数值和等待者的数量。最后一个uint32
用于表示信号量,用于唤醒等待的goroutine。
同步机制
Add
方法:Add
方法用于增加等待计数值。源码如下:
func (wg *WaitGroup) Add(delta int) {
statep, semap := wg.state()
state := atomic.AddUint64(statep, uint64(delta)<<32)
v := int32(state >> 32)
w := uint32(state)
if v < 0 {
panic("sync: negative WaitGroup counter")
}
if w != 0 && delta > 0 && v == int32(delta) {
panic("sync: WaitGroup misuse: Add called concurrently with Wait")
}
for ; w != 0; w-- {
runtime_Semrelease(semap)
}
}
- 通过`atomic.AddUint64`原子操作增加计数值。
- 检查计数值是否为负数,如果是则`panic`。
- 检查是否在`Wait`时并发调用`Add`,如果是则`panic`。
- 释放信号量,唤醒等待的goroutine。
2. Done
方法:Done
方法本质上是调用Add(-1)
,减少等待计数值。
func (wg *WaitGroup) Done() {
wg.Add(-1)
}
Wait
方法:Wait
方法用于阻塞当前goroutine,直到等待计数值变为0。源码如下:
func (wg *WaitGroup) Wait() {
statep, semap := wg.state()
for {
state := atomic.LoadUint64(statep)
v := int32(state >> 32)
if v == 0 {
return
}
w := uint32(state)
if atomic.CompareAndSwapUint64(statep, state, state+1) {
runtime_Semacquire(semap)
if *statep != 0 {
panic("sync: WaitGroup is reused before previous Wait has returned")
}
return
}
}
}
- 使用`atomic.LoadUint64`加载状态值。
- 检查计数值是否为0,如果是则返回。
- 使用`atomic.CompareAndSwapUint64`尝试增加等待者数量。
- 如果成功则通过`runtime_Semacquire`获取信号量,阻塞当前goroutine。
- 检查`WaitGroup`是否被重用,如果是则`panic`。
性能瓶颈优化思路
- 减少不必要的等待:
- 提前规划好任务,避免不必要的
WaitGroup
使用。例如,有些任务可以异步执行且不需要等待其结果,可以直接执行而不通过WaitGroup
同步。 - 如果任务可以并行处理成多个子任务组,可以将大的
WaitGroup
拆分成多个小的WaitGroup
,减少整体等待时间。
- 提前规划好任务,避免不必要的
- 优化计数值操作:
- 在高并发场景下,
Add
和Done
的原子操作可能会成为瓶颈。可以考虑使用更细粒度的锁或者无锁数据结构来优化计数值的增减操作。例如,使用sync/atomic
包中的其他原子操作函数,在保证线程安全的前提下提高性能。
- 在高并发场景下,
- 减少信号量操作:
- 信号量的获取和释放操作(
runtime_Semacquire
和runtime_Semrelease
)会涉及系统调用,开销较大。可以通过优化任务调度,减少信号量操作的频率。例如,采用工作窃取算法,让空闲的goroutine主动获取其他goroutine的任务,减少等待信号量的时间。
- 信号量的获取和释放操作(
- 使用缓存:
- 如果某些任务的结果是可以复用的,可以使用缓存来避免重复计算。这样可以减少任务的执行时间,从而减少
WaitGroup
的等待时间。例如,使用sync.Map
来缓存任务结果。
- 如果某些任务的结果是可以复用的,可以使用缓存来避免重复计算。这样可以减少任务的执行时间,从而减少