文章目录
一、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.
- 分配内存空间给Singleton对象(memory = allocate();)。
- 初始化Singleton对象(ctorInstance(memory);)。
- 将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 单例、并发计数 |
后续会继续补充。
