You need to enable JavaScript to run this app. 为何蒙特卡洛模拟场景下Go并发代码运行速度比串行代码更慢?
优惠活动
大模型
产品
解决方案
定价
更多

为何蒙特卡洛模拟场景下Go并发代码运行速度比串行代码更慢?

问题原因分析
  • 核心原因:全局随机数生成器的锁竞争
    Go标准库math/rand的全局默认随机数实例是并发安全的,底层通过互斥锁保护内部状态。你在experiment函数中调用的rand.Intn本质上每次都要先抢全局锁才能执行,多goroutine并发调用时,大部分时间都消耗在锁等待上,根本没有真正并行执行,反而额外增加了锁调度、上下文切换的开销。这也刚好能解释你观察到的「goroutine数量越少,并发版本性能越高」的现象:goroutine越少,抢锁的冲突概率越低,额外开销越小。
  • 次要原因:任务粒度过细,并发开销占比过高
    单轮模拟实验的逻辑仅为生成随机数+简单判断,计算量极小。你为了执行这些轻量计算,额外付出了goroutine创建调度、通道通信、WaitGroup同步的开销,这些开销最终超过了并行计算带来的收益,导致整体性能反而不如串行版本。
优化方案
  1. 每个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)
}
  1. 进一步优化可以考虑去掉通道通信开销:预先分配一个切片存储每个goroutine的执行结果,所有goroutine写入自己对应下标的位置,全部执行完成后统一求和,进一步减少不必要的通信开销。

优化完成后再运行基准测试,并发版本的性能会接近串行版本的CPU核心数倍,符合你预期的并行加速效果。

内容的提问来源于stack exchange,提问作者jpuriol

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.10 16:21:03