如何构造因编译器优化导致的C++并发Bug最小示例?
如何构造单核环境下编译器优化导致的并发Bug示例
问题背景
在演讲《Real-time Confessions in C++》的6分44秒处,展示了一类并发Bug的核心逻辑:
- 线程A写入未同步的整数x
- 一段时间后,线程B循环读取x
- 线程B无法看到x的更新值,因为编译器优化认为线程B不会修改x,因此在线程启动时仅加载一次x的值。
演讲者旨在证明即使在单核机器上也需同步机制,否则编译器优化会导致代码出错。
我尝试构造该Bug的最小示例,编写了如下代码:
#include <string> #include <iostream> #include <thread> #include <unistd.h> int var; // unsynchronized, i.e., no std::atomic or anything void f() { for (int i = 0; i < 10; i++) { sleep(1); int x = var * var; std::cout << x << std::endl; } } int main() { var = 1; std::thread t(f); sleep(2); var = 10; t.join(); }
预期线程f()中编译器会将var的加载优化为仅在线程启动时执行,从而始终输出1,但实际输出为1后持续输出100。请问我哪里出错了?如何正确构造该并发Bug的最小示例?
问题原因与修复方案
为啥你的代码没复现预期的Bug?
你的代码里有两个细节直接掐断了编译器的优化空间:
std::cout是带同步的操作:std::cout的输出会触发内存屏障,强制线程刷新内存状态,所以线程B每次输出后都会重新读一遍var的最新值,自然能看到主线程改的10。sleep(1)的存在:编译器没法确定sleep内部会不会偷偷修改var(比如通过全局状态),不敢冒险把var的读取提前到循环外面。
正确的最小示例怎么写?
要复现这个Bug,得把所有能触发内存同步的操作都删掉,同时让编译器确信:当前线程不会修改var,也没有其他可见的操作会影响var。
修改后的代码(必须开-O2/-O3优化编译):
#include <thread> #include <unistd.h> int var; volatile int keep_running = 1; // 用volatile确保循环条件不会被优化掉 void worker() { int x; while (keep_running) { x = var * var; // 编译器会把var的读取提到循环外 (void)x; // 告诉编译器x被用到了,别把计算优化没了 } } int main() { var = 1; std::thread t(worker); sleep(2); var = 10; // 修改var,但线程worker看不到 sleep(5); // 等几秒看效果 keep_running = 0; // 让线程退出 t.join(); }
验证方式
用GCC编译时加最高级优化参数:
g++ -std=c++11 -O3 bug.cpp -o bug -pthread
运行后,线程worker会全程使用var=1的值计算,完全看不到主线程设置的var=10——因为编译器会把代码优化成这样:
void worker() { int cached_var = var; // 只读取一次var int x = cached_var * cached_var; while (keep_running) { (void)x; // 循环里啥也不干,反复用缓存的x } }
关键注意点
- 必须用
volatile修饰keep_running,不然编译器会把while(keep_running)优化成死循环,因为它觉得这个变量不会被外部修改。 - 循环里不能加任何I/O、sleep或者系统调用,这些操作都会让编译器放弃激进优化。
- 这个问题的本质是C++内存模型中未同步的数据竞争导致的未定义行为——编译器对于没有同步的变量,有权做任何它认为合理的优化,包括缓存值。
内容的提问来源于stack exchange,提问作者michaeljan
相关产品推荐
相关产品推荐

