这已经是SMP系列心路历程的第十篇了。回想第一次接触对称多处理(SMP)这个概念时,我还是个天天和单核嵌入式处理器打交道的工程师,当时觉得“多核”离自己很远。没想到十几年过去,连普通消费级笔记本都是八核十六线程起步,而我自己也从单纯写业务逻辑,一路折腾到研究缓存一致性、内存序、锁竞争、伪共享这些听起来就让人头大的东西。这篇不算严格教程,更多是把我这些年围绕SMP踩过、填过、思考过的东西做个阶段性的系统梳理,希望对正在往高性能方向走的同行有点用。
1. SMP这十年:从概念到复杂系统的演进
SMP,全称是Symmetric Multi-Processing,对称多处理。简单来说,就是多个处理器核心通过共享主内存和总线来协作,操作系统把每个核心都视为对等的计算资源。单核时代,程序性能主要靠主频和指令集;进入多核时代之后,性能瓶颈经常从“算不快”变成“等不到”——等锁、等内存、等缓存一致性消息。这种转变特别微妙,因为早期很多人想得很简单:核心数加倍,性能自然翻倍。现实则是加速比有一套自己的规律,跟程序里串行部分的占比强相关。Amdahl定律给出一个很直观的公式:加速比 = 1 / ((1 - P) + P / N),其中P是并行部分占比,N是核心数。如果P=90%,16核下的理想加速比也只有不到7倍;只有P达到99%时,16核才能接近13倍。
SMP系统的演进并不只是核心数增加这么简单。过去总线还是共享的,现在普遍使用交叉开关和片间互连;过去内存访问对每个核心完全一致,现在有了NUMA(非均匀内存访问)节点;过去同步靠关中断和自旋,现在靠一套完整的内存模型和同步原语。作为一个开发者,如果你想写出扩展性好的并发程序,光了解API调用是不够的,还需要理解底部硬件在做哪些牺牲和权衡。这一篇会从硬件层面和实践层面一起梳理,覆盖缓存一致性、伪共享、锁竞争、无锁编程,以及一个我在生产环境里做过的真实调优案例。不管你是做中间件、数据库、网关,还是仅仅想把应用的CPU利用率提上来,这些内容大概率都用得上。
1.1 对称多处理到底是什么
SMP最核心的设计思想是“平等”。在传统SMP模型里,每个处理器地位相同,都能访问所有内存和外设,操作系统调度器可以把进程放到任意一个核心上运行,对进程来说,运行在哪个核心不影响逻辑结果。这跟早期非对称多处理(AMP)完全不同,AMP里每个核心有明确分工,比如一个核跑业务,另一个核跑协议栈,应用层不能随意把一个任务丢给任意核心。
不过“平等”只是逻辑层面的表达。现代服务器落地时,为了提高扩展能力,内存访问并不完全均匀,于是出现了NUMA。但即便在NUMA架构里,每个节点上的处理器依然是对称的,只是访问远端内存比本地内存慢。为了实际调优,我们需要分清楚以下几种形态:
| 维度 | AMP(非对称) | SMP(UMA) | NUMA |
|---|---|---|---|
| 核心角色 | 不同核心分工明确 | 各核心完全对等 | 各核心逻辑对等,但内存访问成本不同 |
| 内存访问 | 可能私有或受限 | 一致延迟,无本地概念 | 节点内快,跨节点慢 |
| 调度策略 | 静态指定 | 全局自由调度 | 需要节点感知调度 |
| 扩展能力 | 较低 | 受总线带宽限制 | 较高,适合多路服务器 |
| 典型场景 | 早期嵌入式双核 | 传统4路以内x86服务器 | 双路/四路服务器、众核芯片 |
实际调优时,我建议先用numactl --hardware看清机器的拓扑,确认内存节点类型和CPU分布。很多性能问题不是出在代码逻辑,而是线程被调度到了远端的那个节点,导致每次访存都慢几十纳秒。积累到上百万次操作,差别就会体现出来。
1.2 为什么SMP程序越写越难
从调用明文API到真正写出高性能并发程序,中间隔了一层很厚的知识体系。困难点第一是串行依赖对加速比的影响。业务逻辑往往不是天然并行,总有共享状态,有共享就会有同步,有同步就有竞争和等待。第二是缓存一致性带来的隐形成本。某个核心修改了一个共享变量,其他核心的缓存行会失效,这个失效和重新同步过程非常消耗时间。第三是调试手段失效。并发bug往往需要特定时序才会出现,你在调试器里打上断点,时序变了,问题反而消失了。最后是内存模型问题。C++、Java、Go这些语言都有各自的并发语义,底层CPU的真实行为更复杂,指令重排、store buffer、invalidate queue这些概念如果不了解,写出隐蔽bug的概率非常高。
举个例子,很多年前我在排查一个服务时,发现一个用std::atomic保护的统计计数在8线程下比单线程慢得多。第一反应是原子操作太贵,但换成无锁的per-thread累加后快了很多。后来一查,根本不是原子操作慢,而是多个线程频繁修改同一个内存区域引起的缓存行冲突。这类问题只有理解了底层硬件才能解释清楚。也正因如此,SMP编程的核心能力不是“会用锁”,而是“知道每次同步背后硬件发生了什么”。
1.3 谁应该认真读一读SMP相关的内容
如果你在写业务CRUD,只用了简单线程池,SMP的知识更多是扩展视野;但如果你在做如下几类工作,那必须认真理解:
- 系统软件、中间件、数据库内核开发者。这些场景对并发正确性和性能有双重要求,同步原语用得不好,轻则性能回退,重则线上故障。
- 性能调优工程师。火焰图只是现象,离开SMP底层知识很难判断热点为什么高。
- 游戏引擎、音视频处理、网络转发等实时性要求高的领域。计算密集和延迟敏感,线程调度、缓存布局都直接影响体验。
- 想进入基础设施方向的在校学生。把计算机组成原理、操作系统、编译原理联系起来看,会有种豁然开朗的感觉。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
