
Go b.RunParallel 什么时候才真正并发?
它不会自动并行——必须显式调用 b.SetParallelism(n),否则默认 n=1,所有 goroutine 实际串行执行。这是最常被忽略的前提。
常见错误现象:b.RunParallel 跑完耗时几乎等于单次 b.Run,CPU 利用率不上升,pprof 显示只有 1 个活跃 goroutine。
-
b.SetParallelism(0)是无效的,会被忽略;最小有效值是1 - 推荐设为逻辑 CPU 数(
runtime.NumCPU()),但不是越多越好——过度并发会因调度开销反而拖慢吞吐 - 如果测试函数里有共享资源(如全局 map、未加锁的计数器),不加同步会导致数据竞争,
go test -race会报Data race
如何写一个线程安全的 b.RunParallel 测试函数?
传给 b.RunParallel 的函数接收一个 *testing.PB,它本身不带状态,每次调用都独立;但你写的闭包体里引用的变量,很可能被多个 goroutine 同时访问。
使用场景:压测缓存读取、HTTP 客户端复用、无状态计算函数等。不适合测含全局状态初始化或一次性 setup 的逻辑。
立即学习“go语言免费学习笔记(深入)”;
- 避免在闭包中直接读写包级变量,改用参数传递或局部变量
- 需要统计总数?用
sync/atomic操作int64,别用普通int加锁 - 示例中误写
total++会导致竞态;正确写法是atomic.AddInt64(&total, 1)
func BenchmarkParseJSON_Parallel(b *testing.B) {
data := []byte(`{"name":"a","age":25}`)
var total int64
b.SetParallelism(runtime.NumCPU())
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// 每次解析都用新变量,无共享
var u struct{ Name string; Age int }
json.Unmarshal(data, &u)
atomic.AddInt64(&total, 1)
}
})
}
b.RunParallel 和 b.Run 的性能差异到底看什么?
不能只比总耗时。关键指标是「吞吐量」:单位时间完成的操作数(b.N / b.Elapsed().Seconds()),以及横向扩展能力——当 SetParallelism 从 1 增到 4,吞吐是否接近翻倍?
性能影响点:
- CPU-bound 任务容易随并行度线性提升;I/O-bound 受系统文件描述符、网络连接池限制,可能卡在 2–4 就饱和
- GC 压力会上升——每个 goroutine 都可能分配内存,高频小对象易触发频繁 GC,
go tool pprof -alloc_space可验证 - 注意
b.N是总迭代次数,不是每个 goroutine 执行次数;*testing.PB.Next()控制循环退出,实际各 goroutine 迭代数不均等
为什么本地跑 b.RunParallel 结果波动大,CI 上更差?
因为它的结果高度依赖运行时环境:CPU 频率动态调整、后台进程抢占、NUMA 节点分布、甚至 Go 版本的调度器改进都会改变表现。
兼容性影响:
- Go 1.14+ 调度器对
testing.PB的 work-stealing 更激进,同一份代码在 1.13 和 1.19 上吞吐可能差 15%+ - Docker 容器里没设
--cpus或cgroups限频,runtime.NumCPU()返回的是宿主机核数,但实际只能分到 0.5 核 → 并发反成负优化 - 建议固定
GOMAXPROCS(如GOMAXPROCS=4),并在 CI 中统一关闭 CPU 频率调节(echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor)
真正难控的不是写法,是让并发基准测试在不同机器上可复现——这点连官方文档都没直说,得靠自己锁环境、压负载、看 perf top 确认是不是卡在调度或锁争用上。











