一、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++
實際上是"讀取-修改-寫入"三個步驟的組合操作,可能被其他線程中斷。