面试题答案
一键面试WaitGroup实现原理
- 底层数据结构:
WaitGroup
在Go语言中是一个结构体,其定义在src/sync/waitgroup.go
中。它内部包含一个计数器state1
,这个计数器是一个原子值,用于记录等待的任务数量。同时还有一个信号量semaphore
,用来阻塞和唤醒等待的协程。- 例如,当调用
Add(delta int)
方法时,实际上是原子地增加计数器的值。delta
为正数时增加等待任务数,为负数时减少等待任务数(通常Done()
方法就是调用Add(-1)
)。
- 同步机制:
- 当一个协程调用
Wait()
方法时,如果计数器的值大于0,该协程会被阻塞并放入到信号量队列中。当调用Done()
方法(或Add(-1)
)使得计数器的值变为0时,信号量会唤醒所有在Wait()
中等待的协程。
- 当一个协程调用
性能瓶颈
- 高并发下的原子操作开销:在高并发、大规模协程场景下,频繁对
WaitGroup
的计数器进行原子操作(如Add
和Done
)会带来较大的CPU开销。因为原子操作需要硬件层面的支持,会占用较多的CPU资源。 - 信号量竞争:当大量协程同时调用
Wait()
时,会在信号量上产生竞争。多个协程等待被唤醒,这可能导致上下文切换频繁,降低系统的整体性能。
优化方案及代码示例
- 减少原子操作次数:
- 思路:尽量批量处理
Add
操作,而不是频繁调用Add(1)
。 - 代码示例:
- 思路:尽量批量处理
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
numTasks := 10000
// 批量添加任务
wg.Add(numTasks)
for i := 0; i < numTasks; i++ {
go func() {
defer wg.Done()
// 模拟任务执行
time.Sleep(time.Millisecond)
}()
}
start := time.Now()
wg.Wait()
elapsed := time.Since(start)
fmt.Printf("Total time: %v\n", elapsed)
}
- 使用channel替代部分场景:
- 思路:对于一些固定数量的任务等待场景,可以使用channel来代替
WaitGroup
。channel可以在传递数据的同时进行同步,而且在某些情况下性能更好。 - 代码示例:
- 思路:对于一些固定数量的任务等待场景,可以使用channel来代替
package main
import (
"fmt"
"time"
)
func main() {
numTasks := 10000
doneCh := make(chan struct{}, numTasks)
for i := 0; i < numTasks; i++ {
go func() {
defer func() { doneCh <- struct{}{} }()
// 模拟任务执行
time.Sleep(time.Millisecond)
}()
}
start := time.Now()
for i := 0; i < numTasks; i++ {
<-doneCh
}
close(doneCh)
elapsed := time.Since(start)
fmt.Printf("Total time: %v\n", elapsed)
}
通过以上两种优化方案,可以在高并发、大规模协程场景下提升性能,减少WaitGroup
带来的性能瓶颈。