c++ 学习笔记之 volatile与atomic

文章目录


一、volatile是什么?

volatile是 C++ 中用于修饰变量的关键字,核心作用是「告诉编译器不要对该变量做优化,每次读写都直接访问内存,而非寄存器。

二、使用场景

2.1.硬件寄存器访问

如传感器等外设的地址是固定的,其值会被硬件实时修改(编译器无法感知),必须用 volatile:

cpp 复制代码
// 假设 0x12345678 是温度传感器寄存器地址
volatile uint32_t* temp_reg = (volatile uint32_t*)0x12345678;

int main() {
    while (1) {
        unsigned int timerValue = *temp_reg ;
        // 根据timerValue进行相应操作
    }
    return 0;
}

2.2.多线程中内存可见性

在多线程中,线程可能会更改共享变量,但是编译器优化可能会将共享变量存入缓存中,当线程修改该共享变量时,主线程可能取到的还是缓存中的值,从而导致错误发生。

cpp 复制代码
#include <iostream>
#include <thread>

volatile int value = 0;

void A() {
    value = 100;
    std::cout << "Thread A set sharedValue to: " << sharedValue << "\n";
}

void B() {
    // 等待一段时间,确保Thread A有机会修改sharedValue
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Thread B reads sharedValue as: " << sharedValue << "\n";
}

int main() {
    std::thread a(A);
    std::thread b(B);

    a.join();
    b.join();

    return 0;
}

volatile 关键字可以再线程A修改value 之后强制将值刷新到内存中去,同时也强制线程B从内存中获取最新value 而不是从缓存中获取。

2.3.CUDA中相关应用

GPU 线程块内的共享内存变量,若被多个线程读写,需用 volatile 避免寄存器缓存:

cpp 复制代码
__global__ void kernel(volatile int* shared_data) {
    // 线程0写入
    if (threadIdx.x == 0) {
        shared_data[0] = 100;
    }
    __syncthreads();  //线程同步
    
    // 其他线程读取:volatile 确保读取到最新值
    int val = shared_data[0];
}

cudaHostAlloc 分配的内存,需用 volatile 避免优化:

cpp 复制代码
// 分配可被CPU/GPU同时访问的内存
volatile int* host_dev_ptr;
cudaHostAlloc((void**)&host_dev_ptr, sizeof(int), cudaHostAllocMapped);

2.4.不保证原子性、无法解决指令重排

volatile的主要功能是保证变量的内存可见性(每次读写直接操作内存,不缓存到寄存器),但不能保证原子性。

cpp 复制代码
#include <iostream>
#include <thread>
volatile int counter = 0;

void increment() {
    for (int i = 0; i < 10000; ++i) {
        counter++;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    std::thread t3(increment);

    t1.join();
    t2.join();
    t3.join();
    std::cout << "Final counter value: " << counter << std::endl;
    return 0;
}

counter这个操作不是原子的,它包含了三个步骤:读取counter的值、将值加 1、将结果写回counter。三个线程同时执行counter++,可能会导致所谓的数据竞争(Data Race),也就是丢失更新。

cpp 复制代码
class Singleton {
private:
    static volatile Singleton* instance;
    Singleton() {} 

public:
    static Singleton* getInstance() {
        if (instance == nullptr) {  // 第一次检查
            std::lock_guard<std::mutex> lock(mutex_); 
            if (instance == nullptr) {  // 第二次检查
                instance = new Singleton();  
            }
        }
        return instance;
    }

private:
    static std::mutex mutex_;
};

volatile Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex_;

instance = new Singleton()这行代码可能进行指令重排:即先执行3,在执行2.

  1. 分配内存空间给Singleton对象(memory = allocate();)。
  2. 初始化Singleton对象(ctorInstance(memory);)。
  3. 将instance指向分配好的内存地址(instance = memory;)。
    此时新的线程访问getInstance()时就会返回未初始化的instance。volatile 并未解决指令重排问题。

三、atomic

C++11引入的atomic可以解决这两个问题,同时还能保证内存可见性。

cpp 复制代码
class Singleton {
private:
    static std::atomic<Singleton*> instance;
    static std::mutex mutex_;
    
    Singleton() = default;
    ~Singleton() = default;
    
    class Deleter {
    public:
        ~Deleter() {
            delete instance.load();
        }
    };
    static Deleter deleter;

public:
    static Singleton* getInstance() {
       Singleton* tmp = instance.load(std::memory_order_acquire);
        
        if (tmp == nullptr) {
            std::lock_guard<std::mutex> lock(mutex_);
            tmp = instance.load(std::memory_order_relaxed); // 加锁后无需强约束
            
            if (tmp == nullptr) {
                tmp = new Singleton();
                instance.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }
    
    // 禁止拷贝
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};
std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mutex_;

memory_order_acquire:确保后续的读/写操作不会被重排到这条指令之前,

如果其他线程使用 memory_order_release 存储了指针,本线程能看到完整的初始化结果。memory_order_release:确保对象的构造在存储指针之前完成。

当然,c++11推荐使用静态局部变量,没必要这么麻烦:

cpp 复制代码
class Singleton {
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    static Singleton& getInstance() {
        static Singleton instance;  // C++11 后,这里的初始化是线程安全的
        return instance;
    }
    void doSomething() {
        // TODO
    }
};
维度 volatile std::atomic
设计目标 硬件寄存器 / 内存可见性 多线程原子操作 + 内存序约束
指令重排 不约束 可通过内存序(acquire/release)约束
原子性 无(如 volatile int a++ 非原子) 有(如 a.fetch_add(1) 原子操作)
适用场景 硬件交互、简单内存可见性 多线程共享变量、DCL 单例、并发计数

后续会继续补充。

相关推荐
别了,李亚普诺夫几秒前
OLED显示屏学习笔记
笔记·嵌入式
AI_零食5 分钟前
开源鸿蒙跨平台Flutter开发:密码生成器应用
网络·学习·flutter·华为·开源·harmonyos·鸿蒙
智者知已应修善业8 分钟前
【51单片机1,左边4个LED灯先闪烁2次后,右边4个LED灯再闪烁2次:2,接着所用灯一起闪烁3次,接着重复步骤1,如此循环。】2023-5-19
c++·经验分享·笔记·算法·51单片机
xiaoye-duck14 分钟前
《算法题讲解指南:优选算法-队列+宽搜》--70.N叉树的层序遍历,71.二叉树的锯齿形层序遍历,72.二叉树的最大宽度,73.在每个树行中找最大值
数据结构·c++·算法·队列
代码改善世界15 分钟前
【C++初阶】双向循环链表:List底层结构的完整实现剖析
c++·链表·list
fengci.16 分钟前
LilCTF2025web(前半部分)
开发语言·网络·学习·php
REDcker18 分钟前
C++ 包管理工具概览
开发语言·c++
zhangrelay18 分钟前
蓝桥云课一分钟-绚丽贪吃蛇-后续-cmake
笔记·学习
世人万千丶19 分钟前
Flutter 框架跨平台鸿蒙开发 - AR寻宝探险游戏应用
学习·flutter·游戏·华为·开源·ar·harmonyos
努力努力再努力wz21 分钟前
【C++高阶系列】告别内查找局限:基于磁盘 I/O 视角的 B 树深度剖析与 C++ 泛型实现!(附B树实现源码)
java·linux·开发语言·数据结构·c++·b树·算法