实测:channel 到底比无锁队列慢多少
无锁系列写到 08-1 立了个论断:标准库 channel 用一把锁罩住一切,是"算过账"的选择。但账不能只算不验——这篇把系列里的四种实现搬上同一台机器跑分:channel、SPSC、MPSC mutex、MPMC per-slot。
结论先说:有反转。
1. 环境与方法
CPU: Intel Xeon Gold 6133 @ 2.50GHz, 32 核
Go: go1.24.13 linux/amd64
负载: int64 元素,队列容量 1024,go test -bench,每项 2s × 3 轮取中位
驱动方式:一个 op 记为 一个元素完成完整的 push + pop。队列满时生产者 Gosched 重试、空时消费者 Gosched 重试;channel 版走原生阻塞语义。全部实现在跑分前先过了 -race 验证。
2. 结果
| ns/op | channel | SPSC | MPSC (mutex) | MPMC (per-slot) |
|---|---|---|---|---|
| 1P1C | 135 | 130 | 155 | 112 |
| 4P1C | 158 | — | 335 | 319 |
| 4P4C | 189 | — | — | 440 |
表里的破折号本身就是信息:SPSC 上不了多生产者,MPSC 上不了多消费者——类型即约束,07.md 讲的纪律不是风格偏好,是正确性边界。
3. 三个解读
解读一:单对单,无锁的优势是个位数百分比。 四种实现全部挤在 110~160ns。传说中"channel 慢一个数量级"的印象来自上古时代——Go 1.3 以前 channel 的锁粒度和调度都没优化到位。今天的 channel 在 08-1 拆过的三层快路径加持下,1P1C 只比手写 SPSC 慢 4%。为这点差距放弃关闭语义、select 语义和阻塞语义,不值。
解读二:竞争一上来,局面反转。 4 生产者时 channel 158ns 一骑绝尘,手写队列掉到 320+ns,直接翻倍。原因在 08-2 的谱系里:队列饱和后,channel 的生产者 park(让出 g,档位 5),手写队列的生产者 Gosched 自旋重试(档位 2)——每次失败的重试还要再过一遍锁或 CAS。饱和状态下,等待方式比同步原语本身贵。
解读三:CAS 不是免票。 4P4C 时 Vyukov MPMC 440ns,是 channel 的 2.3 倍。多生产者 CAS 同一个 enqPos、多消费者 CAS 同一个 deqPos,失败的尝试全烧在缓存行乒乓上(08.md §5 讲过的 false sharing,本实现没加 padding——加了会好一些,但翻不了盘)。无锁消掉了锁,没消掉竞争。
4. 边界与坑
- ⚠️ 这是 饱和微基准:生产者永远比消费者快,队列常年顶格。真实业务的写入常有突发和间隙,自旋税会打折
- ⚠️ MPMC 没做缓存行填充,收着 false sharing 的税;补齐 64B padding 后 4P4C 会改善,量级不变
- ⚠️ 首轮数据普遍偏高 20~40%(JIT 式预热、缓存冷启动),所以取三轮中位;单轮 benchmark 骗人,count ≥ 3 是底线
- ✅ 结论的适用边界:默认场景用 channel;只有"绝不允许阻塞"(实时/信号路径)、"单写者热路径"(SPSC 语义天然成立)、或"库代码禁止向用户暴露锁语义"时,才考虑手写无锁队列
小结
跑分把 08-1 的账本坐实了:channel 的锁 + park 组合,在无竞争时和手写无锁打平,在竞争下反超一倍。整个系列的公式也该升级一版——决定吞吐的不是"有没有锁",是 "竞争存不存在、等待怎么付钱":
一句话概括:别问"锁快还是无锁快",问"我的写者能不能只有一个"。这个问题,正是下一篇(也是本系列收官)的主题:把竞争消灭在架构层。
最后更新:2026-09-08