为何蒙特卡洛模拟场景下Go并发代码运行速度比串行代码更慢?
问题原因分析
- 核心原因:全局随机数生成器的锁竞争
Go标准库math/rand的全局默认随机数实例是并发安全的,底层通过互斥锁保护内部状态。你在experiment函数中调用的rand.Intn本质上每次都要先抢全局锁才能执行,多goroutine并发调用时,大部分时间都消耗在锁等待上,根本没有真正并行执行,反而额外增加了锁调度、上下文切换的开销。这也刚好能解释你观察到的「goroutine数量越少,并发版本性能越高」的现象:goroutine越少,抢锁的冲突概率越低,额外开销越小。 - 次要原因:任务粒度过细,并发开销占比过高
单轮模拟实验的逻辑仅为生成随机数+简单判断,计算量极小。你为了执行这些轻量计算,额外付出了goroutine创建调度、通道通信、WaitGroup同步的开销,这些开销最终超过了并行计算带来的收益,导致整体性能反而不如串行版本。
优化方案
- 每个goroutine使用独立的随机数生成器,避免全局锁竞争
给每个goroutine初始化一个专属的rand.Rand实例,用唯一的种子初始化,所有随机数生成本地完成,完全不需要抢全局锁,这是最核心的优化点。修改示例:
// 调整experiment函数,支持传入本地rand实例 func tossEqualToSix(r *rand.Rand) bool { roll := r.Intn(6) + 1 return roll == 6 } // 并发模拟函数调整,每个goroutine创建本地rand func simulationConcurrent(experiment func(*rand.Rand) bool, numTrials, nGoroutines int) float64 { ch := make(chan int) var wg sync.WaitGroup for i := 0; i < nGoroutines; i++ { wg.Add(1) go func(seed int64) { defer wg.Done() localRand := rand.New(rand.NewSource(seed)) localOcurrences := 0 for j := 0; j < numTrials/nGoroutines; j++ { if experiment(localRand) { localOcurrences++ } } ch <- localOcurrences }(int64(i)) } go func() { wg.Wait() close(ch) }() ocurrencesEvent := 0 for localOcurrences := range ch { ocurrencesEvent += localOcurrences } return float64(ocurrencesEvent) / float64(numTrials) }
- 进一步优化可以考虑去掉通道通信开销:预先分配一个切片存储每个goroutine的执行结果,所有goroutine写入自己对应下标的位置,全部执行完成后统一求和,进一步减少不必要的通信开销。
优化完成后再运行基准测试,并发版本的性能会接近串行版本的CPU核心数倍,符合你预期的并行加速效果。
内容的提问来源于stack exchange,提问作者jpuriol
相关产品推荐
相关产品推荐

