登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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 次操作。

Go testing.B RunParallel 中 GOMAXPROCS、worker、PB.Next 与 b.N 总迭代次数的关系说明图
图1:RunParallel 的并发计数口径说明图,展示 worker 共享总迭代次数,不是运行截图。
含义估计吞吐时的作用
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,但要同时记录外部服务限流、连接池大小和锁冲突,否则测到的可能只是排队时间。

RunParallel 中并发度、墙上时间、共享资源竞争与 ops/s 输出之间关系的边界说明图
图2:并发度和吞吐的边界关系说明图,区分调度参数、墙上计时和共享资源竞争,不是性能结果截图。
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 内调用 StartTimerStopTimerResetTimer,这些方法对整个 benchmark 有全局影响。

一张检查表:这个吞吐数字能不能比较

  • 每次 pb.Next() 对应的工作是否一致,是否把初始化误放进计时区间?
  • b.N 是否作为总迭代数使用,而不是乘以 worker 数?
  • 是否记录了 -cpuGOMAXPROCSSetParallelism 和外部资源上限?
  • 是否运行多次并比较趋势,而不是依据单次 ops/s 下结论?

我更愿意把吞吐值当成同一环境下的比较坐标:先固定数据、依赖和基准命令,再改变一个并发参数。如果锁、连接池或服务端限流已经成为瓶颈,继续加并发只是在放大排队,不是在测量真实处理能力。

相关问题

RunParallel 的 b.N 是每个 goroutine 各自的次数吗?

不是。b.N 是整个 benchmark 的总迭代数,pb.Next() 在 worker 之间分配它;计算总吞吐时只使用一次 b.N

SetParallelism(2) 能否说明吞吐提高两倍?

不能。它只把 worker 数调整为 2*GOMAXPROCS,最终速度还受 CPU、锁、连接池、网络和服务端容量影响。

为什么应该在 RunParallel 返回后 ReportMetric?

返回表示并行 body 已完成,此时读取计数和计时结果才是汇总状态;在 worker 内重复上报还会产生覆盖和竞争。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>