多核同步机制:Spinlock、Barrier 与原子操作的设计边界
两个核要修改同一份共享数据,需要防止访问相互干扰;几个核必须各自完成初始化后才能继续,则要确认参与者是否都已到达。这两个场景都可能出现等待,解决的却不是同一个问题。
前者主要是互斥(mutual exclusion),后者是会合(rendezvous)。如果遇到等待就想到加 Spinlock,容易把初始化同步写成持锁忙等,甚至让系统卡在彼此等待的状态。
本文讨论一般多核实时系统的设计方法。AUTOSAR Classic OS 规范定义了 Spinlock 等 OS 服务;Barrier 是这里讨论的通用并发设计概念,不能把供应商专用的 Barrier 服务视为所有 AUTOSAR OS 都具有的标准 API。 文中的流程图和状态案例为原创通用示意,不代表特定商业内核的代码或算法。
1. 先确定要满足的正确性条件
先看设计到底要满足哪种条件:
- 互斥条件:任何时刻,是否最多只允许一个执行者修改受保护对象?
- 会合条件:只有指定的全部执行者都完成上一阶段,是否才能进入下一阶段?
假设两个核分别写共享缓冲区。保护目标是不能在第一个核复制数据到一半时,让第二个核同时覆盖或读取一个不一致的快照。这适合研究临界区与锁。
另一个场景中,几个核各自初始化自己的数据结构,随后需要同时开始处理依赖全局状态的任务。即使各自初始化的对象互不相同,也仍然可能需要同步点。这时只用互斥锁并不能表达“大家到齐”。
| 概念 | 保护目标 | 常见行为 | 主要风险 |
|---|---|---|---|
| 原子操作 | 对某个操作提供不可分割的观察语义 | 硬件原子读改写 | 不能自动保护整个多步骤事务 |
| Spinlock | 临界区互斥 | 抢锁失败则忙等待 | CPU 消耗、优先级反转、死锁 |
| Barrier | 多个参与者的阶段会合 | 全部到齐才放行 | 缺席参与者、轮次错误、忙等待 |
| Event | 表示条件或事件发生 | 等待任务可能进入 Waiting | 丢失/合并语义、触发与业务数据脱节 |
| Memory Barrier | 约束内存访问顺序 | CPU/编译器层面的排序约束 | 不能单独完成互斥或会合 |
Memory Barrier 与本文的同步屏障 Barrier 虽然名字相近,却处于不同层次。
2. Spinlock 为什么依赖原子操作?
Spinlock 的基本要求是:两个核同时尝试获取同一把锁时,至多一个核可以成功成为持有者。
如果先读一个普通变量,判断它为“空闲”,再写成“占用”,两个核可能同时读到空闲、都进入临界区。问题不在于单次 32 位读写是否会被拆开,而在于“判断并占用”是一个跨越多个步骤的读改写事务。
因此,典型实现利用硬件原子读改写操作,例如 Compare-and-Swap 或 Exchange。AURIX TC3xx 的公开手册列出了 SWAP.W、CMPSWAP.W、LDMST 等原子指令;具体合法地址、对齐与内存属性必须按目标平台规定使用。不能因为指令原子,就认定整个 128 字节缓冲复制也是原子的。
使用 Spinlock,至少要守住两条规则:
- 只有成功获得锁的执行者才能进入临界区。
- 临界区中所有应当受保护的访问,必须统一遵守同一把锁的协议。
第二点经常被忽视。写入者加锁、读取者不加锁,通常仍不能保证读取的是完整一致的数据;具体是否有风险,还要分析任务可达性和其它同步约束。
2.1 持锁时间与等待时间不是一回事
锁竞争时,等待者已经消耗了时间,却未执行有用的业务;持锁者如果被高优先级上下文抢占,则其他核可能持续等待。把两者混成一个“函数耗时”,会掩盖问题来源。
性能分析至少要分开观察:
| 指标 | 起止位置 | 说明 |
|---|---|---|
| Lock Wait | 请求锁 → 成功获得锁 | 反映竞争和持有者进度 |
| Lock Hold | 获得锁 → 释放锁 | 反映临界区占用 |
| 数据访问时间 | 开始读写 → 完成读写 | 不等于全部持锁时间 |
| 端到端时延 | 业务事件产生 → 结果被消费 | 还包含调度和队列等待 |
因此,临界区设计通常应尽可能短,并符合 OS 对持锁期间服务调用、抢占以及中断控制的规定。AUTOSAR OS 规范对 Spinlock 的持有和释放、嵌套顺序、错误返回均有约束。
3. Barrier 解决的是“有没有全部到齐”
Barrier 的条件不是“锁当前是否空闲”,而是“本轮所有指定参与者是否都已经到达”。
可以用四个独立执行者举例:
阶段 A:各自初始化
Core A ──────────┐
Core B ──────┐ │
Core C ─────────────┐
Core D ─────────┐ │
▼ ▼
[ 会合条件 ]
│
全部到齐后进入阶段 B这个示意只表达逻辑条件,不意味着各核在同一个时钟周期同时执行下一条指令。Barrier 放行以后,实际执行时间仍受调度、指令流水线和处理器进度影响。
3.1 可以只使用一个原子变量吗?
对于仅执行一次、参与者固定且每人恰好到达一次的场景,可以。 一种通用思路是:所有参与者原子增加到达计数;计数等于参与者数量时,等待者得知条件满足。
如果参与者拥有稳定且互不重复的编号,也可以使用原子位图,各自设置自己的到达位。位图还有一个实际好处:调试时能够直接看出谁没有到达。
不过,计数达到参与者总数,还不足以说明同步实现一定正确。至少还要满足两个前提:
- 共享状态的并发更新必须是正确的原子操作,不能用普通的
++替代多写者的原子读改写。 - 到达前写入的数据是否对其它核可见,要由完整的内存同步协议保证,不能只看计数值是否正确。
这说明 Barrier 不一定需要 Spinlock。原子状态本身就可以承载会合协议。
3.2 为什么不能持有 Spinlock 等待大家到齐?
假设 Core A 抢到了锁,在临界区里增加到达计数,然后仍持锁等待计数达到 4;其余三个核必须获得同一把锁才能更新计数。
结果是:A 不释放锁,其他人无法报告“已经到达”,计数永远不能达到 4。这是典型的自我封闭等待条件。
正确设计原则是:如果要用锁保护到达计数,锁只覆盖更新过程;进入等待前必须释放该锁。 等待状态的读写还需要合适的原子性和可见性保证。
4. Barrier 为什么比一个计数器复杂?
一次性 Barrier 只需要确认这一轮是否到齐。反复使用时,还得分清各参与者处于第几轮:
| 时间 | 执行者 A | 执行者 B | 潜在问题 |
|---|---|---|---|
| 第 1 轮 | 已经到达 | 最后到达 | 所有人可通过 |
| 轮次边界 | A 很快进入第 2 轮 | B 还在离开第 1 轮 | 双方对轮次认知不同 |
| 第 2 轮 | A 等待 B | B 可能尚未报告新一轮到达 | 不能把第 1 轮的状态误当新一轮 |
如果第一次到达计数达到 N 以后始终保持不变,第二轮可能被错误放行;直接粗暴地清零也可能与仍在读取上一轮状态的参与者冲突。
通用方案通常要引入“轮次”或“阶段”信息,例如 generation、sense-reversing 等算法家族。它们都要处理参与者到达、轮次切换、释放条件、状态复用之间的关系;如果支持参与者动态加入或退出,设计会更加复杂。
需要注意,有限宽度的轮次计数还存在回绕问题。某些模序列号比较策略只有在参与者进度差保持在规定范围内时才有明确顺序语义。因此不能仅看到有限位宽计数器和比较表达式,就断言它在任何延迟、重启、脱队情形下都安全。
所以审查 Barrier,既要看它如何正常放行,也要说明算法成立的前提和失败条件。
5. 忙等待与 Task 阻塞为什么不同?
在 AUTOSAR Classic OS 中,Extended Task 可以通过 WaitEvent 等待事件,进入 Waiting 状态,允许调度器按照就绪情况选择别的 Task 运行。
自旋等待则不同:执行者在循环里反复检查条件,通常仍占有 CPU 执行时间。是否会被高优先级任务或中断抢占,取决于运行上下文及实际 OS 的抢占、中断和锁方法。
这一差别在多核 Barrier 中尤其重要。假设一个 Core 上有两个必须同时到达的任务:高优先级任务 A 正在自旋等低优先级任务 B,但 B 必须先获得这个 Core 的执行机会才可能到达。若 A 的执行方式始终阻止 B 调度,就可能造成永久等待。
因此不能因为“所有参与者最后总会走到函数”就认定自旋 Barrier 可以在任意 Task 间使用。应先证明参与者的调度进度得到保证。
5.1 没有 Timeout,就一定会被 Watchdog 复位吗?
不能这样推导。
等待循环没有超时,只能说明等待原语本身没有主动放弃或返回错误的机制。Watchdog 是否能够覆盖这种等待,取决于喂狗上下文、监督实体、WdgM 主函数是否能继续运行,以及实际硬件看门狗的配置。
设计时要分别描述:
- 同步原语有没有超时。
- 缺席参与者是否允许退出本轮。
- 等待过程是否阻塞看门狗服务路径。
- 超时或故障恢复由哪一层负责。
不能把外部 Watchdog 当成 Barrier 正确性的证明。
6. Memory Barrier、volatile 和 NonCached 各负责什么?
多核共享数据至少涉及四类常被混淆的问题:
互斥:能否防止多个执行者同时进入临界区?原子性:某次操作能否被观察为不可分割?可见性:一个核完成写入后,其他核何时能观察到?顺序:其他核能否以预期顺序观察到多次访问?
volatile主要约束编译器对相应访问的处理,不是通用的跨核同步协议。- NonCached 可以避免某些缓存一致性问题,但不能把多步骤数据复制变成一个原子事务。
- Memory Barrier 和同步相关的指令可用于满足内存顺序要求,但单独执行它们并不能阻止另一个核同时修改数据。
- Spinlock 可把一组访问串行化,正确实现通常还需要配套的内存序和硬件访问约束。
针对 AURIX TC3xx,原子访问的有效地址、对齐、内存区域与指令语义应核对 Infineon 官方用户手册。不能把对某个 CPU 的结论无条件移植到所有 MCU。
7. 多核同步怎么选?
| 需求 | 首先考虑的机制 | 需要特别检查 |
|---|---|---|
| 小范围共享数据修改 | Spinlock 或其它满足条件的同步 | 临界区时长、读写双方是否遵守协议 |
| 一次性多核初始化会合 | 一次性 Barrier | 参与者固定、状态初始化、到达保证 |
| 周期性计算阶段同步 | 可复用 Barrier 或更适合调度的机制 | 轮次、等待方式、缺席者处理 |
| Task 等待业务条件 | Event/WaitEvent 等 OS 机制 | Task 类型、事件位、调度和错误状态 |
| 只需某个标志或单值更新 | 适合的原子操作 | 原子宽度、内存序和读写角色 |
| 需要传递数据并通知消费者 | 队列或受保护缓冲 + 通知 | 数据发布顺序、队列溢出、消费者可达性 |
不要只根据数据字节数选锁,也不要因为使用了 NonCached RAM 就省略并发分析。
设计审查的一个有效顺序是:先列出所有参与者和共享对象,再列出每个访问的读写方向,随后检查同步前提与调度可达性,最后才讨论性能优化。
8. 怎样验证机制真的生效?
看见源码、生成代码和 ELF 符号,并不代表已经证明其真实运行行为。
对于 Spinlock,应关注锁等待、持有时间、失败返回,以及是否所有访问都遵守相同的锁协议。对于 Barrier,应关注实际参与者、到达顺序、等待时间、轮次变化和异常情况下的恢复。
实时调试还需要特别小心:单独停止一个核,可能人为让其他核停留在 Barrier 中;在等待链中打断点,也可能让原本不存在的时延成为主要现象。断点、Trace 与时间基准要依据调试器能力和系统安全约束选择。
静态可以解释可能发生什么,运行证据才可以说明目标系统在某次执行中实际发生了什么。
9. 小结
Spinlock 解决“同一时刻谁可以进入临界区”,Barrier 解决“谁已经到达同一个阶段”。原子操作是构建这些协议的底层工具,Memory Barrier 是内存访问顺序工具,Event 则是任务同步与调度机制。它们互有联系,却不能用一个名称替代另一个。
如果还没有建立 OS Application 的归属与保护模型,可先阅读 OS Application 的对象归属与保护机制。若想进一步理解共享数据和业务事件的差异,可以接着看 COM Deferred EventCache 的跨核一致性。
公开资料与适用范围
- AUTOSAR CP R24-11:Specification of Operating System:Spinlock、Task/Event、保护及跨核 OS 服务的规范语义。
- Infineon AURIX TC3xx 官方用户手册 Part 1:原子访问、对齐与硬件架构。
- AUTOSAR Classic Platform 官方介绍:平台和架构范围。
本文只解释可从公开资料及通用并发理论建立的设计原理,不含供应商私有同步实现,也不含任何特定 ECU 的运行时测量结论。