06 / 80 · C++11 · 约 8 分钟
volatile:可观察访问,不是线程同步
volatile 告诉实现相关访问具有可观察效果,不能按普通内存访问随意消除。它不提供原子性、线程间顺序或 happens-before,也不承诺绕过 CPU 缓存;线程通信应使用原子操作或锁。
约束的是访问语义
通过 volatile 限定的访问路径读取或写入对象,属于实现必须按相应规则保留的可观察访问。它的典型应用是平台定义的内存映射设备寄存器,以及满足专门限制的信号处理场景,而不是给普通变量增加“更实时”的属性。
哪些硬件地址有效、一次访问对应何种总线操作、是否需要设备屏障,都需要实现和平台文档。标准 C++ 不承诺 volatile 必须绕过 CPU 缓存,也不承诺任意两条普通内存访问会因附近出现 volatile 而建立硬件顺序。
为何不能代替 atomic
volatile 读写既不自动成为不可分割操作,也不建立线程间的 happens-before。两个线程对同一个 volatile int 无同步地冲突访问,仍然可能构成数据竞争;反复读取一个 volatile 标志也不能安全发布旁边的普通数据。
共享计数器应使用 mutex 或 std::atomic。若只是统计且不发布其他状态,原子 fetch_add 配合 relaxed 常常足够;如果标志负责发布数据,则要设计 release/acquire 等同步关系。关键在于明确通信协议,不是机械地把关键字替换掉。
用安全示例划清边界
示例在单线程内写入和读取一个局部 volatile 对象,演示合法的受限定访问;另用原子计数器展示独立的原子操作。它不伪造设备地址、不启动有数据竞争的线程,也不把这段程序声称为真实硬件驱动验证。
C++20 起,volatile 自增、自减以及若干复合赋值等用法被弃用。新代码不要靠 volatile ++x 表达同步。对信号处理还须遵守异步信号安全限制,常见 volatile std::sig_atomic_t 的有限用途也不能推广为普通多线程共享状态。
容易答错的地方
- volatile 不是缓存刷新指令;用它解释“每次从主内存读取”会混淆语言抽象与具体机器。
- 不要去掉真正 volatile 对象的限定再通过普通访问路径读取;这种绕过会破坏语言要求,而非仅降低优化等级。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <atomic>
#include <cassert>
#include <iostream>
int main() {
volatile int observed = 0;
observed = 7;
const int snapshot = observed;
std::atomic<int> count{0};
const int previous = count.fetch_add(1, std::memory_order_relaxed);
assert(snapshot == 7);
assert(previous == 0);
assert(count.load(std::memory_order_relaxed) == 1);
std::cout << snapshot << ' ' << count.load() << '\n';
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread basics-volatile.cpp -o example && ./example预期结果
7 1
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
工作线程写 data = 42 后设置 volatile bool ready,主线程等待 ready 再读 data,这个方案为何错误?给出正确的标志协议。
查看参考答案
volatile ready 自身就不是线程安全的同步对象,也不保护 data。可把 ready 改为 std::atomic<bool>,工作线程先写 data,再 ready.store(true, std::memory_order_release);读线程等到 ready.load(std::memory_order_acquire) 返回 true 后读取 data。要求 data 没有随后未同步的写入,此时发布与获取建立可见性关系。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。