Go testing.B RunParallel 如何估计并发吞吐
来源:17golang原创
时间:2026-09-15 17:27:37 284浏览 收藏
我第一次用 testing.B.RunParallel 时,最容易误读的是“开了多少个 goroutine”与“每秒完成多少次操作”之间的关系。RunParallel 默认按 GOMAXPROCS 创建 worker,把同一个 b.N 拆给它们;真正要估计并发吞吐,应在并行工作结束后用总迭代次数除以实际计时窗口,而不是拿 worker 数当吞吐。
pb.Next()在所有 worker 之间合计执行b.N次,b.N是总操作数。- 并发吞吐可按
float64(b.N) / b.Elapsed().Seconds()估算,再用ops/s报告。 - CPU 密集型任务先比较
-cpu,非 CPU 密集型任务才考虑SetParallelism(p);并发度提高不保证吞吐线性增长。
先把 RunParallel 的三个数量分开
RunParallel 的 worker 数默认是当前 GOMAXPROCS,调用 SetParallelism(2) 后才变为 2*GOMAXPROCS。这只是参与调度的 goroutine 数。每个 worker 都调用同一个 body,但 pb.Next() 负责从共享迭代计数中领取工作,所以所有 worker 加起来才覆盖 b.N 次操作。

| 量 | 含义 | 估计吞吐时的作用 |
|---|---|---|
GOMAXPROCS | 默认并行度基数 | 影响 worker 数,不等于每秒操作数 |
SetParallelism(p) | 把 worker 数调为 p*GOMAXPROCS | 用于等待型或外部依赖型负载的对比 |
b.N | 所有 worker 合计完成的迭代数 | 作为吞吐计算的总操作数 |
在 RunParallel 返回点计算 ops/s
Go 文档明确说明,RunParallel 报告的 ns/op 是整个并行基准的墙上时间,不是各 goroutine CPU 时间相加。要得到更直观的吞吐,可以在 RunParallel 返回后读取 b.Elapsed()。此时所有 worker 已结束,计算结果不会读到中间状态。
func BenchmarkParallelWork(b *testing.B) {
// 这里的工作体量应保持稳定,避免把随机等待当成吞吐变化。
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// 每次 Next 代表一次被计入 b.N 的基准操作。
doWork()
}
})
seconds := b.Elapsed().Seconds()
if seconds > 0 {
// b.N 是所有 worker 的总操作数,除以墙上时间得到 ops/s。
b.ReportMetric(float64(b.N)/seconds, "ops/s")
}
}
这个指标表达的是“该次基准计时窗口内完成的总操作数/秒”。不要把它解释成单个 goroutine 的速度,也不要把一次短跑的结果当作固定容量。go test -bench BenchmarkParallelWork -cpu=1,2,4 可以观察不同处理器并行度下的变化。
并发度提高后,为什么吞吐可能不再上升
如果 doWork 主要消耗 CPU,继续增加 worker 往往只会带来调度和竞争开销;这类场景通常先用 -cpu 比较。若操作包含网络、磁盘或锁等待,才有理由尝试 SetParallelism,但要同时记录外部服务限流、连接池大小和锁冲突,否则测到的可能只是排队时间。

func BenchmarkParallelIO(b *testing.B) {
// 非 CPU 密集型示例才考虑增加 worker,p 不是吞吐倍率。
b.SetParallelism(2)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// 外部依赖的延迟、连接池和限流都应在结论中单独记录。
doOneRequest()
}
})
if elapsed := b.Elapsed().Seconds(); elapsed > 0 {
// 在所有并行调用结束后一次性写入汇总指标。
b.ReportMetric(float64(b.N)/elapsed, "ops/s")
}
}
如果还要统计“成功操作数”或字节数,计数器必须使用 atomic.Int64 等并发安全类型,并在返回后读取。不要在 worker 内调用 StartTimer、StopTimer 或 ResetTimer,这些方法对整个 benchmark 有全局影响。
一张检查表:这个吞吐数字能不能比较
- 每次
pb.Next()对应的工作是否一致,是否把初始化误放进计时区间? b.N是否作为总迭代数使用,而不是乘以 worker 数?- 是否记录了
-cpu、GOMAXPROCS、SetParallelism和外部资源上限? - 是否运行多次并比较趋势,而不是依据单次 ops/s 下结论?
我更愿意把吞吐值当成同一环境下的比较坐标:先固定数据、依赖和基准命令,再改变一个并发参数。如果锁、连接池或服务端限流已经成为瓶颈,继续加并发只是在放大排队,不是在测量真实处理能力。
相关问题
RunParallel 的 b.N 是每个 goroutine 各自的次数吗?
不是。b.N 是整个 benchmark 的总迭代数,pb.Next() 在 worker 之间分配它;计算总吞吐时只使用一次 b.N。
SetParallelism(2) 能否说明吞吐提高两倍?
不能。它只把 worker 数调整为 2*GOMAXPROCS,最终速度还受 CPU、锁、连接池、网络和服务端容量影响。
为什么应该在 RunParallel 返回后 ReportMetric?
返回表示并行 body 已完成,此时读取计数和计时结果才是汇总状态;在 worker 内重复上报还会产生覆盖和竞争。
-
467 收藏
-
426 收藏
-
374 收藏
-
151 收藏
-
101 收藏
-
Golang · Go教程 | 46分钟前 | 时区 · Go教程 · time.Date · 夏令时 · 时间验证 · Go time.Date Go 夏令时日期验证 Go time.LoadLocation Go 重复小时 Go 时区偏移校验162 收藏
-
468 收藏
-
376 收藏
-
Golang · Go教程 | 1小时前 | testing · Go教程 · benchmark · ReportMetric · 性能指标 · 基准测试 Go 自定义指标 testing.B ReportMetric411 收藏
-
398 收藏
-
349 收藏
-
251 收藏
-
466 收藏
-
Golang · Go教程 | 2小时前 | Go教程 · 结构化日志 · log/slog · JSONHandler · ReplaceAttr · Go slog JSONHandler时间格式 HandlerOptions ReplaceAttr slog.TimeKey Go结构化日志时区 time.Time Format158 收藏
-
204 收藏
-
112 收藏
-
341 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习