14 共享数据与竞争条件
核心:多个任务读写同一份数据时,要考虑同步;
volatile不是锁。
1. 为什么 counter++ 会丢失更新?
初始 counter = 0
任务 A:读取 0 ── 计算 1 ────────── 写回 1
任务 B: 读取 0 ── 计算 1 ───── 写回 1
期望:2 可能结果:1
counter++ 是读 → 修改 → 写回,不能凭这一行代码判断它具有原子性。在有 C11 数据竞争语义的环境里,未同步的冲突访问还可能构成未定义行为。
2. Mutex 的作用
任务 A ── 拿锁 ── 读/改/写 ── 释放锁
任务 B ── 等待 ─────────────── 拿锁 ── 读/改/写 ── 释放锁
// 示意伪代码:实际调用名称取决于 RTOS
mutex_lock();
counter++;
mutex_unlock();
整段读—修改—写都应受同一把锁保护,所有访问者要遵守相同约定。
| 选择 | 好处 | 代价/限制 |
|---|---|---|
| Mutex | 能保护多个任务共享数据 | 可能等待,带来调度及锁开销 |
| 无同步直接修改 | 不需要锁开销 | 存在竞争、丢失更新风险 |
3. ISR 与任务:不要在中断里等普通 Mutex
蓝牙硬件中断
│
▼
ISR:快速处理必要硬件状态 + 发送 ISR 安全通知
│
▼
ISR 退出
│
▼
蓝牙任务:等待通知 → 获取 Mutex → 处理共享数据 → 释放 Mutex
为什么? 如果 ISR 打断了持锁任务,再阻塞等待这把锁,持锁任务可能无法运行去释放它。ISR 通常应快速返回,使用 RTOS 专门提供的 ISR 安全 API 通知任务。通知方式要能匹配事件数量和数据生命周期。
4. volatile vs Mutex:不要混淆
volatile | Mutex | |
|---|---|---|
| 做什么 | 约束对易变对象访问的编译器优化 | 协调任务进入临界区 |
能保证 counter++ 原子吗 | 不能 | 不是把操作变原子指令,而是提供互斥保护 |
| 典型用途 | 硬件寄存器等 | RTOS 任务共享状态 |
volatile int counter;
counter++; // 有 volatile 仍不代表多任务修改安全
一句话总结
- 竞争:两个任务可能都读旧值,最后丢一次更新。
- Mutex:让任务排队,保护整个共享操作。
- ISR:快进快出,通知任务处理,不等待普通 Mutex。
- volatile:不是同步机制,不能代替 Mutex 或原子操作。
注:示意图及 API 为教学用途,实际代码以芯片、RTOS 的同步原语和 ISR API 为准。