【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 的直观区别。

相关推荐
say_fall1 分钟前
【Linux系统编程】Ext 文件系统:磁盘与存储、文件与目录、现代计算机究竟如何寻找文件
linux·运维·服务器·数据库·c++
雪落漂泊20 分钟前
C++算法 滑动窗口篇(下)
开发语言·c++
爱聊灵光的维C35 分钟前
从看见世界到重构认知:在视觉与硬件的交汇处,寻找我们这代人的北极星
c语言·物联网·计算机视觉·重构·硬件工程·射频工程
AI+程序员在路上1 小时前
RV1126B 上支持的模型及全流程应用--从CNN及语音到 LLMVLM 与跨平台选型
linux·c语言·人工智能
揽星河..2 小时前
【C语言】扫雷游戏
c语言·算法·游戏
你怎么知道我是队长3 小时前
C++零基础第3周:if/else、switch、for/while/do-while 控制流完全指南(附20段可运行代码+避坑清单)
java·开发语言·c++
冻柠檬飞冰走茶3 小时前
PTA刷题集之Airline Routes(C语言实现)
c语言·开发语言·数据结构·算法
青少儿编程课堂4 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:倍增法求最近公共祖先LCA解析
c++·python·算法·bfs·信息学竞赛
冻柠檬飞冰走茶4 小时前
PTA刷题集之Battle Over Cities - Hard Version(C语言实现)
c语言·开发语言·数据结构·算法
雷✘5 小时前
C++ 类与对象的初始化机制、成员访问边界与拷贝优化
java·网络·c++