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

后续会继续补充。

相关推荐
Zsy_0510032 小时前
【C++】类和对象(二)
开发语言·c++
宵时待雨2 小时前
STM32笔记归纳2:GPIO
笔记·stm32·嵌入式硬件
啊阿狸不会拉杆2 小时前
《机器学习》第四章-无监督学习
人工智能·学习·算法·机器学习·计算机视觉
Duang007_2 小时前
【万字学习总结】API设计与接口开发实战指南
开发语言·javascript·人工智能·python·学习
啊阿狸不会拉杆2 小时前
《机器学习》第三章 - 监督学习
人工智能·深度学习·学习·机器学习·计算机视觉
GeekyGuru3 小时前
C++跨平台开发的核心挑战与应对策略
开发语言·c++
Howrun7773 小时前
信号量(Semaphore)
开发语言·c++·算法
sjg200104143 小时前
GoFrame学习随便记3(待续)
学习
橘子师兄3 小时前
C++AI大模型接入SDK—ChatSDK使用手册
开发语言·c++·人工智能