跳转至

实测:channel 到底比无锁队列慢多少

无锁系列写到 08-1 立了个论断:标准库 channel 用一把锁罩住一切,是"算过账"的选择。但账不能只算不验——这篇把系列里的四种实现搬上同一台机器跑分:channel、SPSCMPSC mutexMPMC 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 组合,在无竞争时和手写无锁打平,在竞争下反超一倍。整个系列的公式也该升级一版——决定吞吐的不是"有没有锁",是 "竞争存不存在、等待怎么付钱"

单写者(无竞争)→ 什么同步都便宜,选语义最顺手的
多写者(有竞争)→ 消灭竞争(归并写者)> park 等待(channel)> 自旋等待(手写队列)

一句话概括:别问"锁快还是无锁快",问"我的写者能不能只有一个"。这个问题,正是下一篇(也是本系列收官)的主题:把竞争消灭在架构层。


最后更新:2026-09-08

评论