1. 是什么
atomic_int 是 C11 标准提供的原子整数类型 。它保证对变量的"读"和"写"是不可分割 的(原子),
且并发访问时不会撕裂、不会乱序。
c
atomic_int g_list_ready = ATOMIC_VAR_INIT(0); // 初始化 = 0
atomic_store(&g_list_ready, 1); // 原子写 1
int val = atomic_load(&g_list_ready); // 原子读
2. 打比方:记账本
想象一个多人共用的记账本(内存变量):
-
普通
int:记账本是"活页纸"。老板(任务A)正在写账,店员(任务B)一把抢过纸开始写------
两张纸都被涂花,账目撕裂。 -
atomic_int:记账本被换成带锁的磁性白板 。每个人读/写必须"整笔操作"完成,
写一半不可能被抢走------每次读到的都是完整的一笔账。普通 int: atomic_int:
┌─────────┐ ┌──────────────┐
│ 写一半被抢 ✗ │ 写就是一笔 ✓│
│ 读到脏数据 ✗ │ 读就是完整 ✓│
└─────────┘ └──────────────┘
关键 :白板保证的是"这一笔账不会花",不保证 "账本和仓库货物一致"------那是锁(info_take/info_give)的职责。
3. 为什么要用:并发撕裂问题
在嵌入式里,同一个变量常被中断 和任务同时访问:
【任务A】 【中断(如UART接收)】
g_ready = 1; ←→ if (g_ready) {...}
普通 int 的"读-改-写"在底层是多条指令,中间随时可能被打断:
asm
LDR R0, [addr] ; ① 读
ADD R0, R0, #1 ; ② 加 ← 这里被打断,抢占者读到了旧值!
STR R0, [addr] ; ③ 写
结果:抢占者看到"旧值",两边状态错乱。这就是 Race Condition(竞态)。
atomic_int 的 load/store 在 32 位对齐访问下单条指令完成,天然不被撕裂。
4. 底层原理:LDREX / STREX(读-改-写的保险锁)
对于"读-改-写"类原子操作(如 count++、compare_exchange),硬件用 LDREX/STREX 排他访问:
步骤① LDREX R0, [addr] 读取 + 给地址贴"独占标签"
步骤② 中间被中断打断...... 标签还在,写入作废
步骤③ STREX R1, R2, [addr] 尝试写:
- 标签还在 → 写成功,R1=0
- 标签丢了 → 写失败,R1=1,重来一遍
c
// atomic_int 的 fetch_add 底层大致长这样(编译器生成,无需手写)
do {
old = __LDREXW(addr); // ① 读 + 贴标签
} while (__STREXW(old + 1, addr)); // ③ 标签在才写,失败重试
比喻:先占座,再结账。占座期间被插队,就放弃重排。
5. 有什么作用(清单)
| 作用 | 说明 | 本项目用途 |
|---|---|---|
| 原子性 | 读/写/RMW 不可分割,不撕裂 | g_list_ready 读写 |
| 可见性 | 写后其他任务/中断立即看到新值 | 锁故障标志即时通知全店 |
| 顺序保证 | 默认seq_cst,前后指令不乱序 |
先写缓冲数据 → 再置标志 |
| 无需关中断 | 排他指令不关中断,不阻塞实时性 | 中断里也能安全用 |
| 免锁开销 | 32 位原子是 lock-free,比信号量快 | 高频状态标志 |
6. 什么场景用(对照本项目)
✅ 该用:多执行体共享的"小状态标志"
本项目 <sys_var_ring_ls.c> 里的这两个就是典型:
c
static atomic_int g_list_ready; // 全店就绪标志:中断/任务都要读
static atomic_int g_list_lock_error; // 全店锁故障标志:多处写、多处读
特征:值小(0/1) 、读写频繁 、任务和中断都可能碰 、对实时性敏感。
✅ 该用:中断里也要操作的共享计数/标志
c
atomic_int rx_overrun_cnt; // 中断里 ++,任务里读清零
❌ 不该用:大的数据缓冲、结构体数组
数据区(ring 缓冲)不能用原子,必须用锁保护:
c
sys_var_ring_write(&info->ring, ...); // 数据缓冲 → 靠 info_take/info_give 锁
原则:原子管"标志",锁管"数据"。
❌ 不该用:只有单一写者、且无中断竞争
单写者单读者且不会被抢占撕裂时,普通 int + volatile 就够了,用原子反而多操心。
7. 顺序保证:为什么"先写数据再置标志"安全
atomic_store 默认是 seq_cst(最强顺序),编译时会插入内存屏障,保证:
写者:先写 ring 数据(普通写) → DMB屏障 → atomic_store(标志=1)
读者:atomic_load(标志==1) → DMB屏障 → 再读 ring 数据
写者线程 读者线程
│ 写数据① │
│ 置标志② (release) │
│ 读标志② 已见 ✓
│ 读数据① 必见 ✓ ← 屏障保证不乱序
比喻:先摆货(数据),再开门(标志)。顾客看到"营业中"时,货一定已经摆好。
8. 常见 API 速查
| API | 作用 | 示例 |
|---|---|---|
ATOMIC_VAR_INIT(x) |
初始化 | ATOMIC_VAR_INIT(0) |
atomic_store(p, v) |
原子写 | atomic_store(&g_ready, 1); |
atomic_load(p) |
原子读 | if (atomic_load(&g_ready)) {...} |
atomic_fetch_add(p, n) |
原子加 | atomic_fetch_add(&cnt, 1); |
atomic_compare_exchange |
CAS 比较交换 | 实现自旋锁 |
9. 注意事项(结合本项目)
- 单核 Cortex-M7 可见性无忧 :所有任务/中断同 CPU 同 Cache,
atomic_store后其他执行体立即可见。 - 本项目 SRAM0/SRAM1 为 Cacheable:CPU 侧读写一致;DMA 缓冲都在 Non-Cacheable 的 AXI SRAM,互不干扰,无需手动 cache 维护。
- 编译器要求 AC6(armclang) :
<stdatomic.h>是 C11 特性,老编译器 AC5(ARMCC5)不支持,会编译报错。本项目已是 AC6 V6.16。 atomic_int是 lock-free:32 位原子不依赖 RTOS,中断里也能用,不会导致死锁。- 不要拿原子操作替代业务锁 :数据缓冲一致性永远靠锁(
info_take/info_give),原子只保护标志本身。
10. 一张图总结
┌─────────────────────────────────────────────┐
│ sys_var_ring_ls 业务池 │
│ │
│ 原子标志(atomic_int) 数据缓冲(ring) │
│ g_list_ready info->ring │
│ g_list_lock_error info->lock │
│ ↓ 原子、立即可见 ↓ 锁保护 │
│ 状态同步 数据一致 │
└─────────────────────────────────────────────┘
记住三句话:
- 原子 = 白板记账,一笔不会花;
- 标志用原子,数据用锁;
- 单核 CM7 下,原子写完立即看得见。