QQ三公机器人 无锁编程在实际项目中好用吗

---


一句话回答

好用,但有严格的适用范围。用对了是利器,用错了是灾难。


它既不是银弹,也不是屠龙术。下面直接从实战角度拆开说。


---

一个真实案例


某推理网关,几十个 worker 线程抢一个任务队列。火焰图上 llllockwait 常年霸榜前三。换成无锁 Treiber 栈,QPS 涨了四成,上线后在 x86 集群跑了整整一年,一次事故都没有。[citation:87.16]


另一个案例:百万级 QPS 的金融交易系统,加锁方案下锁争抢消耗了 40% 以上的 CPU。团队花了三周做无锁改造才把吞吐拉上去。[citation:87.4]


但同一批人后来在 ARM 平台部署同样的无锁代码,出了诡异的数据错乱——内存模型不一样,x86 上跑得好的逻辑在 ARM 上不成立。[citation:87.16]


---

什么时候推荐用


| 场景 | 理由 | 典型例子 |

|:----|:------|:---------|

| 高争抢的热点路径 | 锁等待占总 CPU >20% | 消息队列、任务分发、计数器 |

| 单写多读 | 天然无需互斥 | 配置更新、状态发布 |

| 极短操作(纳秒级) | CAS 比锁上下文切换快几个数量级 | 引用计数、指针替换 |

| 不能阻塞的上下文 | 中断/信号处理函数里不能休眠 | 内核路径、实时系统 |

| 核心基础设施 | 被千万次调用的底层库 | 无锁队列、内存分配器 |


---

什么时候不要碰


| 场景 | 原因 |

|:----|:------|

| 普通业务逻辑 | 用锁足够,无锁的调试成本远超收益 |

| 复杂数据结构 | 无锁红黑树?写出来也没人敢 review |

| 跨平台部署 | x86 强内存模型掩盖了问题,ARM/PowerPC 上暴雷 |

| 团队没有并发专家 | 无锁 bug 极其隐蔽,跑几个月才出现一次 |

| 没有 perf 数据支撑 | "觉得锁慢"不是理由,先 profile 确认 |


---

四大陷阱(每一个都值一个线上事故)


| 陷阱 | 现象 | 本质 |

|:----|:-----|:------|

| ABA 问题 | CAS 读到 A→改 B→改回 A,你以为没变过 | 需要加版本号解决 |

| 伪共享(False Sharing) | 性能不升反降,甚至比加锁还慢 | 不同线程的变量碰巧在同一条 CacheLine 上 |

| 半初始化发布 | 拿到指针但对象构造到一半 | 缺少 acquire/release 屏障 |

| 单边屏障失效 | x86 正常,ARM 上数据错乱 | 只对自己的变量加屏障,忽略了对面线程的视角[citation:87.16] |


---

实战建议

① 优先用造好的轮子


不需要自己手写无锁数据结构——标准库里基本都有现成的:


| 语言 | 直接可用的无锁容器 |

|:----|:------------------|

| C++ | std::atomic<>, std::atomic_flag, boost::lockfree::queue/stack |

| C# | ConcurrentQueue, ConcurrentDictionary, Interlocked |

| Go | atomic 包,channel 本身就是无锁队列的封装 |

| Java | AtomicInteger, ConcurrentLinkedQueue, LongAdder |

② 先测量,再动手

Linux 上看锁争抢

perf top -p <pid> # 看看 llllockwait 占比

perf record -e sched:sched_switch -p <pid>


如果锁争抢不到 10% 的总 CPU,优化锁不如优化别处。

③ 从简单的开始


推荐路径:

SPSC(单生产者单消费者)队列 → MPSC → MPMC


SPSC 队列实现极其简单(一个 head 指针 + 一个 tail 指针),没有 CAS 争抢,性能极高,而且不需要解决 ABA 问题。大部分"需要一个高性能无锁队列"的场景,SPSC 就够用。

④ 复杂的无锁算法要做好"回退"计划


无锁内存池、无锁哈希表这类重型武器,建议设计时保留退化到加锁路径的能力——线上出诡异常规手段查不出来时,一把切回加锁保命,回头慢慢排查。


---

性能对比(参考值)


| 同步机制 | 延迟(典型) | 备注 |

|:--------|:----------:|:-----|

| CAS(无争抢) | ~10ns | 硬件指令,极快 |

| CAS(有争抢) | ~100ns–1μs | 自旋等待 |

| Mutex(无争抢) | ~25ns | 快速路径 |

| Mutex(有争抢) | ~1–10μs | 上下文切换 |

| 系统调用锁(futex) | ~1–5μs | 进入内核态 |


从有锁换成无锁,在不争抢时可能只快一倍;但在高争抢下差距可以是 两个数量级(锁让 CPU 空转在上下文切换上,CAS 让 CPU 空转在自旋上,后者比前者轻得多)。


---

六、总结


| | 加锁 | 无锁 |

|:--|:-----|:------|

| 开发难度 | 低 | 极高 |

| 调试难度 | 中等 | 地狱级 |

| 高争抢性能 | 差(上下文切换) | 好(自旋) |

| 低争抢性能 | 好 | 好 |

| 可移植性 | 好 | 差(内存模型依赖) |

| 正确性证明 | 人能 review | 需要形式化验证 |

| 适合团队 | 所有团队 | 有并发专家 |

问你一个问题:如果你的无锁代码上线后每三个月崩一次,需要一个小团队花一周才能定位,你还会用它吗? 决定是否用无锁,技术判断只占一半,另一半是对你自己和团队的了解。


0 评论

发表评论