【C/C++】多线程读写普通 int 变量的一些问题(二)

下面是一段 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 即可彻底消除。

关键点回顾

  1. 对齐32位int,单条读写指令是原子,不会撕裂;
  2. 但编译器优化、CPU缓存、指令重排序,会导致读不到最新值、逻辑错乱;
  3. C++:不要用volatile做线程同步,优先std::atomic或者mutex。

如果你想,我可以进一步解释 memory_order_relaxed / acquire / release / seq_cst 的直观区别。

相关推荐
别动我齐刘海8 小时前
Linux 系统调试学习1
linux·运维·服务器·c++·学习·机器学习·机器人
wuyk55512 小时前
嵌入式驱动开发:量产级工程化实战(第9篇)——低功耗下的通信可靠性
c语言·驱动开发·stm32·单片机·嵌入式硬件
学逆向的12 小时前
C 突破编译器限制
c语言·开发语言·网络安全
jimy112 小时前
C++ 虚函数表:基类和派生类共用一张 vtable 吗?
开发语言·c++
爱吃提升12 小时前
Dev-C++ 5.11 C++断点调试完整教程
开发语言·c++
xlq2232213 小时前
全面复习7
c++
endeavor1213 小时前
can总线(五)
c语言·stm32·单片机·can
geats人山人海14 小时前
现代c++2.4 虚继承
java·开发语言·c++
csuzhucong14 小时前
天元一号、二号、三号
c++
灯澜忆梦14 小时前
【面向对象编程c++】| 数组与函数
java·数据结构·c++