49、原子操作:C11原子操作与volatile的区别

这个问题,我几乎每次培训都会被问到。

很多人觉得 volatile 和原子操作差不多,都能用在多线程里。嗯,这个理解其实有点危险。我见过不止一个项目,因为搞混了这两个东西,线上出了诡异的bug,查了好几天才定位到。

今天咱们就把这事彻底说清楚。

volatile 到底干了什么?

volatile 这个关键字,说白了就是告诉编译器:别优化我

编译器有时候很聪明,它会觉得某个变量在循环里没被改过,就直接从寄存器里读,不去内存取了。这在单线程里没问题,但一旦有中断或者另一个线程改了那个变量,编译器还傻乎乎地用寄存器里的旧值,就出事了。

volatile 就是强制编译器每次读写都去内存里拿,别耍小聪明。

volatile 的核心能力:

  • 防止编译器优化掉对变量的读写
  • 保证每次读取都从内存获取最新值
  • 保证每次写入都立即写回内存

但是,请注意——volatile 不保证原子性

什么意思?你写一个 volatile int,在32位系统上可能是一条指令完成的。但如果你写一个 volatile long long,在32位系统上就需要两条指令。这时候线程切换进来,你就只改了半个变量,数据就坏了。

C11 原子操作又是什么?

C11 标准引入了 stdatomic.h,提供了真正的原子操作。它保证两件事:

  • 原子性:操作要么全部完成,要么完全不执行,不会看到中间状态
  • 内存序:可以控制不同线程间看到操作的顺序

我个人习惯,只要涉及多线程共享变量,优先用原子操作,而不是 volatile

我的经验: 在嵌入式裸机开发中,如果只是中断和主循环共享一个标志位,用 volatile 就够了。但一旦涉及多核或者RTOS多线程,我建议直接用原子操作。

一张图看懂区别

下面这张图,是我自己总结的。你看完应该就明白了。

volatile vs C11原子操作 核心区别 volatile ✅ 防止编译器优化 ✅ 保证内存可见性 ❌ 不保证原子性 ❌ 不保证操作顺序 ❌ 不支持内存序控制 适用场景: 中断标志位、内存映射寄存器 单核裸机、简单共享 C11 原子操作 ✅ 保证原子性(读-改-写) ✅ 保证内存可见性 ✅ 支持多种内存序 ✅ 防止编译器优化 ✅ 跨平台一致性 适用场景: 多线程计数器、状态标志 多核通信、锁实现 功能递增

代码对比:一看就懂

咱们直接看代码。假设两个线程都在对一个计数器做自增操作。

用 volatile 的错误写法

// 错误示例:volatile 不保证原子性
volatile int counter = 0;

// 线程A
counter++;  // 实际是:读-加-写 三步

// 线程B
counter++;  // 可能在线程A写回之前就读了旧值

你想想看,counter++ 在底层是三条指令:读内存、寄存器加1、写回内存。如果线程A刚读完,线程B也读了,两个都加1,然后都写回——结果只加了1次,不是2次。

我曾经踩过的坑: 在一个多核ARM芯片上,用 volatile 修饰了一个共享计数器,结果跑着跑着计数就对不上了。查了两天才发现,两个核同时执行自增,volatile 根本拦不住这种竞争。

用 C11 原子操作的正确写法

// 正确写法:使用原子操作
#include <stdatomic.h>

atomic_int counter = 0;

// 线程A
atomic_fetch_add(&counter, 1);

// 线程B
atomic_fetch_add(&counter, 1);

atomic_fetch_add 保证整个读-改-写操作是原子的。要么全部完成,要么没做。不会出现中间状态。

内存序:volatile 做不到的事

C11 原子操作还有一个大杀器——内存序

你可以控制原子操作的内存可见性顺序。比如:

// 宽松内存序:只保证原子性,不保证顺序
atomic_store(&flag, 1, memory_order_relaxed);

// 释放-获取语义:保证之前的写入对其他线程可见
atomic_store(&flag, 1, memory_order_release);
// 另一个线程
int val = atomic_load(&flag, memory_order_acquire);

volatile 完全做不到这个。它只能保证编译器不优化,但CPU还是可能乱序执行。在多核系统上,你用一个 volatile 标志位去同步,另一个线程可能看到乱序的结果。

我的建议: 如果你不确定用什么内存序,就用默认的 memory_order_seq_cst。它最安全,虽然性能稍微差一点,但不会出错。等你有经验了再优化。

什么时候用 volatile?什么时候用原子操作?

场景 推荐方案 原因
中断服务函数和主循环共享标志位 volatile 单核,中断不会被打断,volatile 足够
内存映射寄存器 volatile 硬件寄存器值可能被外设改变,需要禁止优化
多线程计数器 原子操作 需要保证自增操作的原子性
多线程状态标志 原子操作 需要内存序控制,保证可见性
实现自旋锁 原子操作 需要 test-and-set 等原子指令
单核裸机简单共享 volatile 没有多线程竞争,volatile 够用

避坑指南

我总结几个常见的坑,你遇到了可以少走弯路:

  • 别用 volatile 实现锁——它保证不了原子性,锁会坏掉
  • 别以为 volatile 能跨线程同步——它只防编译器,不防CPU乱序
  • 原子操作不是万能的——复杂的数据结构还是需要锁
  • 注意平台差异——有些单片机不支持原子操作指令,C11原子操作会退化成锁

我曾经犯过的错: 在一个Cortex-M0的芯片上用了原子操作,结果发现它没有LDREX/STREX指令,编译器默默给我加了个全局锁。性能直接掉了一半。后来我改成关中断的方式,反而更快。

嗯,总结一下:volatile 是给编译器看的,原子操作 是给CPU和内存系统看的。两者不是一个层面的东西。多线程编程,优先用原子操作;单核中断共享,volatile 就够了。别混着用,也别互相替代。


公众号:蓝海资料掘金营,微信deep3321