一、volatile
volatile
是C++中一个非常重要的关键字。volatile
关键字告诉编译器,被修饰的变量可能会在程序控制之外被改变,因此编译器不能对该变量的访问进行优化。什么意思呢?现代处理器架构中,有寄存器,L1
缓存,L2
缓存,L3
缓存,内存这种架构,可以发现,为了提高访问速度,会将计算的中间变量直接保存在缓存中,再慢慢刷新到内存。
这么做当然提高了访问速度,但是!但是!但是!数据被修改后是不能直接反馈到内存的,这样做会存在一些问题。
1.1、volatile
的基本含义
volatile
关键字告诉编译器,被修饰的变量可能会在程序控制之外被改变,因此编译器不能对该变量的访问进行优化。
volatile int flag = 0;
- 读取可见性:每次读取
volatile
变量都必须从内存中读取,不能使用寄存器和三级缓存中的缓存值 - 写入可见性:每次写入
volatile
变量都必须立即写入内存 - 顺序性:对
volatile
变量的操作不会被编译器重排序(但处理器仍可能重排序)
1.2、volatile
的主要用途
1.2.1、硬件寄存器访问
在嵌入式系统中,硬件寄存器通常映射到内存地址,其值会由硬件改变:
volatile unsigned int *reg = (unsigned int *)0x1234;
*reg = 1; // 写入寄存器
int val = *reg; // 必须从寄存器读取,不能使用缓存值
1.2.2、多线程共享变量
在C++11之前,volatile
有时被用来实现线程间共享变量(注意:这不是标准推荐的做法):
volatile bool ready = false;
// 线程1
void producer() {
ready = true; // 告诉消费者数据已准备好
}
// 线程2
void consumer() {
while(!ready); // 等待数据准备好
}
1.2.3、信号处理程序中的变量
信号处理程序(Signal Handler)是操作系统提供的一种异步事件处理机制,用于响应系统或程序运行时发生的各种事件(称为"信号")。它是Unix/Linux系统编程和C/C++底层编程中的重要概念。
信号是操作系统向进程发送的异步通知,用于通知进程发生了某种事件。常见信号包括:
SIGINT
(2):终端中断信号(通常是Ctrl+C)SIGSEGV
(11):段错误(非法内存访问)SIGTERM
(15):终止信号SIGALRM
(14):定时器信号SIGUSR1
(10)/SIGUSR2
(12):用户自定义信号
当变量可能在信号处理程序中被修改时:
volatile sig_atomic_t signal_received = 0;
void handler(int) {
signal_received = 1;
}
在信号处理程序中使用volatile
关键字修饰变量是为了解决编译器优化可能导致的可见性问题,确保信号处理程序与主程序之间能够正确通信。
1.2.4、volatile与const的结合
volatile
可以和const
结合使用,表示变量在程序内不可修改,但可能被外部修改:
const volatile int hardware_clock = 0x1234;
1.3、volatile的局限性
不是线程安全的:
volatile
不提供原子性保证,不能替代std::atomic
不保证内存顺序:不提供内存屏障或顺序一致性
不阻止处理器重排序:仅阻止编译器优化,不限制处理器行为
二、std::atomic
std::atomic
是C++11引入的模板类,为多线程编程提供了真正的原子操作支持,解决了volatile
在多线程环境中的不足。
2.1、定义
std::atomic
提供了一种线程安全的方式来访问和修改共享数据:
#include <atomic>
std::atomic<int> counter(0); // 原子整型变量
2.1.1、基本原子操作
std::atomic<int> val;
val.store(42); // 原子存储
int x = val.load(); // 原子加载
int y = val.exchange(43); // 原子交换
bool success = val.compare_exchange_strong(expected = x, desired = 44);
// 比较交换,比较原子变量的当前值是否与 expected 相等
// 如果相等,则将原子变量的值设置为 desired,并返回 true
// 如果不相等,则将 expected 更新为原子变量的当前值,并返回 false
2.1.2、原子算数运算(仅限整型)
std::atomic<int> count(0);
count.fetch_add(1); // 原子加
count.fetch_sub(1); // 原子减
count++; // 等价于fetch_add(1)
2.1.3、基本原子操作(仅限整型)
std::atomic<int> flags(0);
flags.fetch_and(0x0F); // 原子与
flags.fetch_or(0x01); // 原子或
2.2、内存顺序详解
C++原子操作允许指定内存顺序,控制操作的可见性和顺序性:
enum memory_order {
memory_order_relaxed, // 最宽松,只保证原子性
memory_order_consume, // 数据依赖顺序
memory_order_acquire, // 获取操作
memory_order_release, // 释放操作
memory_order_acq_rel, // 获取-释放操作
memory_order_seq_cst // 顺序一致性(默认)
};
2.2.1、 memory_order_seq_cst
(顺序一致性)
- 最强保证:所有线程看到的内存操作顺序一致
- 性能开销:最大
- 默认选择:如果不确定就用这个
举个顺序一致性的例子:
std::atomic<int> x(0), y(0);
// 线程1
x.store(1, std::memory_order_seq_cst); // A
y.store(1, std::memory_order_seq_cst); // B
// 线程2
int r1 = y.load(std::memory_order_seq_cst); // C
int r2 = x.load(std::memory_order_seq_cst); // D
可能的执行顺序:
- A → B → C → D (r1=1, r2=1)
- A → C → B → D (r1=0, r2=1)
- C → D → A → B (r1=0, r2=0)
但不会出现 r1=1 且 r2=0 的情况,因为这违反顺序一致性。每个seq_cst
操作都相当于一个全内存屏障(full memory fence),阻止屏障前后的任何内存操作跨越屏障
2.2.2、memory_order_acquire
(获取语义)
- 适用场景:读操作(加载)
- 保证:当前操作之后的所有内存访问(包括非原子操作)不会被重排序到它前面
- 效果:获取其他线程释放的内容
2.2.3、memory_order_release
(释放语义)
适用场景:写操作(存储)
保证:当前操作之前的所有写操作不会被重排序到它后面
效果:释放内容给其他获取的线程
举个获取语义与释放语义的例子:
std::atomic<bool> ready(false);
int data = 0;
void producer() {
data = 42; // (1) 非原子写入
ready.store(true, std::memory_order_release); // (2) 原子释放存储
}
void consumer() {
while (!ready.load(std::memory_order_acquire)) { // (3) 原子获取加载
// 忙等待
}
assert(data == 42); // (4) 保证成立
}
2.2.4、memory_order_acq_rel
(获取-释放)
- 适用场景:读-修改-写操作(如fetch_add)
- 保证:同时具有acquire和release语义,保证操作之后的所有内存访问不会被重排序到它前面,保证操作之前的所有内存访问不会被重排序到它后面
2.2.5、memory_order_consume
(消费语义)
- 类似acquire但更弱:只保证依赖该加载操作的数据不被重排序
- 较少使用:多数情况下用acquire更安全
2.2.6、memory_order_relaxed
(宽松顺序)
- 最弱保证:只保证原子性,不保证顺序
- 适用场景:计数器等不需要同步的场景
2.3、std::atomic
的局限性
- 原子操作不是免费的,比非原子操作慢
- 复杂的同步问题可能需要结合其他同步机制(如互斥锁)
三、考点
3.1、volatile
和 std::atomic
的区别
特性 | volatile | atomic |
---|---|---|
线程安全 | 不保证 | 保证 |
原子性 | 不保证 | 保证 |
内存顺序 | 无 | 可控制 |
适用场景 | 硬件寄存器、信号处理 | 多线程同步 |
volatile仅保证:
- 编译器不会优化掉对变量的访问
- 每次访问都从内存读取/写入
但不保证:
- 操作的原子性
- 多线程环境下的可见性和顺序性
3.2、什么是原子操作,为什么需要原子操作
原子操作是指在多线程或并发环境中不可分割的操作,它要么完全执行,要么完全不执行,不会被其他线程或进程中断的操作。
原子操作主要解决并发编程中的竞态条件(Race Condition)问题:
- 保证操作的完整性:防止操作被其他线程中断导致数据不一致
- 避免竞态条件:确保共享资源的正确访问
- 实现线程安全:不需要使用锁的情况下保证线程安全
- 提高性能:相比锁机制,原子操作通常有更高的性能
3.3、C++11中的 std::atomic
提供了哪些基本原子类型?
C++提供了以下基本原子类型:
std::atomic<bool>
std::atomic<char>
std::atomic<int>
std::atomic<long>
std::atomic<long long>
std::atomic<bool>
std::atomic<char>
std::atomic<unsigned int>
std::atomic<unsigned long>
std::atomic<unsigned long long>
3.4、请用 std::atomic
实现一个简单的自旋锁
class SpinLock {
std::atomic<bool> flag{false};
public:
void lock() {
while(flag.exchange(true, std::memory_order_acquire)) {
// 自旋等待
}
}
void unlock() {
flag.store(false, std::memory_order_release);
}
};
3.5、原子操作和互斥锁(mutex)有什么区别?各自适用什么场景?
特性 | 原子操作 | 互斥锁 |
---|---|---|
实现方式 | 无锁 | 基于锁 |
阻塞 | 非阻塞 | 可能阻塞 |
适用范围 | 简单数据类型 | 复杂操作/多变量 |
性能 | 更高 | 较低 |
死锁风险 | 无 | 有 |
中断安全性 | 安全 | 不安全 |
适用场景:
- 原子操作:计数器、标志位、简单状态等
- 互斥锁:复杂数据结构、需要保护多个变量的操作等
3.6、volatile能否保证操作的原子性?为什么?
不能。volatile只能保证内存可见性(每次读取都从内存获取最新值),但不能保证操作的原子性
例如,volatile int i = 0; i++;
这样的操作在多线程环境下仍然是不安全的,因为i++
实际上是"读取-修改-写入"三个步骤的组合操作,可能被其他线程中断。