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

相关推荐
晓蛋1 天前
c语言指的是什么意思
c语言·编译器·编程开发·集成开发环境·程序实例
倒头就睡的小比特1 天前
算法竞赛C++常用的STL
c++·算法
weilx12341 天前
C++笔记-文件IO-<fcntl.h>
c++
傲世仙尊1 天前
目录即文件-Ext文件系统收尾篇
linux·c语言
牵猫散步的鱼儿1 天前
重载、重写(覆盖)、重定义区别
c语言
phltxy1 天前
C 语言指针:从内存地址到灵活的数据访问
c语言
Smileyqp沛沛1 天前
前端?C++ ?较大差异基础罗列
c++·基础·前端转c++
C语言小火车1 天前
C/C++ 为什么需要编译器?
开发语言·c++
phltxy1 天前
C 语言中的数据存储:从类型到二进制位
c语言
旖旎夜光1 天前
力控面试题 01.01: 判定字符是否唯一(位运算) —— 题解
c++·学习·算法·leetcode·力控