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

相关推荐
一只旭宝17 分钟前
五种线程池设计
c++·笔记
luj_176822 分钟前
毒素驱动的生物自毁机制探析
服务器·c语言·开发语言·经验分享·算法
某林21224 分钟前
机器人收不住、转不动?执行器死区的原理与三层补偿设计
前端·网络·c++·架构·机器人
程与留15 小时前
15_国际化和本地化:tr()、ts 文件、QM 文件、多语言切换
c++·qt
程与留16 小时前
14_Qt 样式表(QSS)入门(语法、选择器、美化实战)
c++·qt
欧特克_Glodon1 天前
OpenCV计算机视觉开发入门与实践<二十七>:图像分割概述
c++·人工智能·opencv·计算机视觉
蒸蒸yyyyzwd1 天前
cpp 选手秋招学习笔记 day21
c++·面试·八股
「QT(C++)开发工程师」1 天前
C++ auto 用法详解
开发语言·c++
OPEN-F2 天前
C++STL教程:容器适配器与实用工具
开发语言·c++