下面是一段 C++ 可复现代码,演示:普通 int,一个线程写,一个线程读,读线程永远看不到新值(编译器优化 + CPU缓存可见性问题)。
编译环境:g++ / clang,必须开启优化 -O2,不加优化现象不容易复现。
cpp
#include <iostream>
#include <thread>
int g_val = 0;
void writer()
{
// 写线程:2秒后修改全局变量
std::this_thread::sleep_for(std::chrono::seconds(2));
g_val = 100;
std::cout << "writer: set g_val = 100\n";
}
void reader()
{
// 读线程循环读取 g_val
while (g_val == 0)
{
// 空循环,什么都不干
}
// 如果能看到100,才会走到这里打印
std::cout << "reader: g_val changed to " << g_val << std::endl;
}
int main()
{
std::thread t_write(writer);
std::thread t_read(reader);
t_write.join();
t_read.join();
return 0;
}
编译&运行
bash
g++ main.cpp -O2 -pthread -o test
./test
现象:
writer: set g_val = 100
程序不会退出 ,reader 的循环卡死,永远看不到 g_val=100。
原因:
编译器优化发现 reader 线程里没有修改 g_val,直接把 while(g_val ==0) 优化成死循环,只读取一次寄存器里的旧值,不再访问内存。
注意:这是编译器优化,还没到CPU缓存那一层,是最直观的可见性问题。
方案1:加上 volatile(仅阻止编译器优化,仍有CPU重排序风险)
cpp
volatile int g_val = 0;
- 效果:reader 会去内存读,不再被编译器优化死循环。
- 局限:volatile 不提供内存屏障,CPU硬件重排序依然存在,多线程逻辑依然可能出错,C++里不要拿volatile做线程同步。
方案2:std::atomic 正确写法 ✅
cpp
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> g_val{0};
void writer()
{
std::this_thread::sleep_for(std::chrono::seconds(2));
g_val.store(100);
std::cout << "writer: set g_val = 100\n";
}
void reader()
{
while (g_val.load() == 0)
{
}
std::cout << "reader: g_val changed to " << g_val.load() << std::endl;
}
int main()
{
std::thread t_write(writer);
std::thread t_read(reader);
t_write.join();
t_read.join();
return 0;
}
编译 -O2,运行输出:
writer: set g_val = 100
reader: g_val changed to 100
程序正常退出。
std::atomic 默认使用 memory_seq_cst 顺序,自带内存屏障,解决:编译器优化、CPU缓存可见性、指令重排序。
再演示指令重排序的例子
这个例子模拟"标记+数据",普通int下会出现:flag已经为1,但data还是旧值。
cpp
#include <iostream>
#include <thread>
int data = 0;
int flag = 0;
void thread_a()
{
data = 999;
flag = 1;
}
void thread_b()
{
while (flag == 0) {}
// 理论预期 data==999;重排序下可能读到 data==0
std::cout << "data = " << data << std::endl;
}
int main()
{
for(int i=0;i<10000;++i)
{
data = 0;
flag = 0;
std::thread ta(thread_a);
std::thread tb(thread_b);
ta.join();
tb.join();
}
return 0;
}
重排序现象不一定每次必现,是概率性bug,很难调试。改成
std::atomic<int> data,flag即可彻底消除。
关键点回顾
- 对齐32位int,单条读写指令是原子,不会撕裂;
- 但编译器优化、CPU缓存、指令重排序,会导致读不到最新值、逻辑错乱;
- C++:不要用volatile做线程同步,优先
std::atomic或者mutex。
如果你想,我可以进一步解释 memory_order_relaxed / acquire / release / seq_cst 的直观区别。