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 单例、并发计数

后续会继续补充。

相关推荐
wregjru7 小时前
【QT】4.QWidget控件(2)
c++
浅念-7 小时前
C++入门(2)
开发语言·c++·经验分享·笔记·学习
ZH15455891317 小时前
Flutter for OpenHarmony Python学习助手实战:面向对象编程实战的实现
python·学习·flutter
小羊不会打字7 小时前
CANN 生态中的跨框架兼容桥梁:`onnx-adapter` 项目实现无缝模型迁移
c++·深度学习
Max_uuc7 小时前
【C++ 硬核】打破嵌入式 STL 禁忌:利用 std::pmr 在“栈”上运行 std::vector
开发语言·jvm·c++
简佐义的博客7 小时前
生信入门进阶指南:学习顶级实验室多组学整合方案,构建肾脏细胞空间分子图谱
人工智能·学习
近津薪荼7 小时前
dfs专题4——二叉树的深搜(验证二叉搜索树)
c++·学习·算法·深度优先
rannn_1118 小时前
【苍穹外卖|Day4】套餐页面开发(新增套餐、分页查询、删除套餐、修改套餐、起售停售)
java·spring boot·后端·学习
艾莉丝努力练剑8 小时前
【Linux:文件】Ext系列文件系统(初阶)
大数据·linux·运维·服务器·c++·人工智能·算法
张人玉8 小时前
VisionPro 定位与卡尺测量学习笔记
笔记·学习·计算机视觉·vsionprp