Wiki
std::atomic
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
std::atomic<T> 用來對一個共享對象執行原子操作,定義在:
#include <atomic>
“原子”表示某個操作在併發觀察下不可被拆成一半看到。例如一個原子自增不會出現另一個線程只看到“讀取完成但寫回還沒完成”的中間狀態。
但要特別注意:
std::atomic保證原子語義,不等於保證底層實現一定 lock-free。
是否真正無鎖取決於類型、平台和標準庫實現。
1.為什麼普通變量不夠
下面的代碼存在數據競爭:
int counter = 0;
void add_many()
{
for (int i = 0; i < 100000; ++i)
{
++counter;
}
}
如果多個線程同時執行 ++counter,結果並不可靠。
對於這種“單個獨立計數器”的情況,可以使用:
std::atomic<int> counter{0};
2.最基礎的 load() 和 store()
原子對象可以顯式讀取和寫入:
std::atomic<int> value{0};
value.store(10);
int x = value.load();
很多簡單場景也允許寫成類似普通變量的形式:
value = 10;
int x = value;
但在學習原子語義時,顯式寫 load() / store() 更容易看出這裏發生的是原子訪問。
3.原子自增與 fetch_add()
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
std::atomic<int> counter{0};
void add_many()
{
for (int i = 0; i < 100000; ++i)
{
++counter;
}
}
int main()
{
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i)
{
threads.emplace_back(add_many);
}
for (auto& thread : threads)
{
thread.join();
}
std::cout << counter.load() << '\n';
}
還可以顯式使用:
counter.fetch_add(1);
常見原子讀改寫操作包括:
fetch_add()
fetch_sub()
fetch_and()
fetch_or()
fetch_xor()
其中位運算版本主要用於整數原子類型。
4.exchange()
exchange() 會原子地:
- 寫入新值;
- 返回舊值。
std::atomic<int> value{10};
int old = value.exchange(20);
執行後:
old == 10
value == 20
這種“替換並取回舊值”的操作在狀態切換裏很常見。
5.std::atomic<bool>:狀態標記
#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>
std::atomic<bool> running{true};
void worker()
{
while (running.load())
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
std::cout << "stopped\n";
}
int main()
{
std::thread t(worker);
std::this_thread::sleep_for(std::chrono::milliseconds(350));
running.store(false);
t.join();
}
這種簡單停止標記很適合原子變量。
但如果使用 C++20,還應該瞭解後面的 std::jthread 和 std::stop_token,因為它們提供了更結構化的協作式停止機制。
6.atomic 不等於“整個對象線程安全”
如果一個類有多個字段:
std::atomic<int> x;
std::atomic<int> y;
只能保證對 x 和 y 各自的原子操作不會撕裂。
它並不會自動保證:
x 和 y 永远表示同一个逻辑状态
例如:
x.store(10);
y.store(20);
另一個線程完全可能在兩次寫入之間觀察到:
x == 10
y == 旧值
如果多個變量必須作為整體保持一致,通常應該使用同一把 mutex 保護整個狀態。
7.compare_exchange:比較並交換(CAS)
CAS 是很多無鎖算法的核心操作。
概念上它做的是:
如果 atomic 当前值 == expected
atomic = desired
返回成功
否则
expected = atomic 当前值
返回失败
C++ 提供:
compare_exchange_weak()
compare_exchange_strong()
示例:
#include <atomic>
#include <iostream>
int main()
{
std::atomic<int> value{10};
int expected = 10;
bool success = value.compare_exchange_strong(expected, 20);
std::cout << std::boolalpha << success << '\n';
std::cout << value.load() << '\n';
}
輸出:
true
20
如果 expected 不匹配:
std::atomic<int> value{10};
int expected = 5;
bool success = value.compare_exchange_strong(expected, 20);
那麼:
success == false
value 仍然是 10
expected 被更新成 10
7.1.weak 和 strong
compare_exchange_weak() 允許“偽失敗”:即使值相等,也可能返回失敗。
因此它很適合放在循環中:
int expected = value.load();
while (!value.compare_exchange_weak(expected, expected + 1))
{
}
compare_exchange_strong() 不允許這種偽失敗,更適合只嘗試一次的場景。
8.std::atomic_flag
std::atomic_flag 是一種非常基礎的原子標誌類型。
它常見的兩個操作是:
test_and_set()
clear()
可以用來實現最簡單的自旋鎖思想:
std::atomic_flag flag = ATOMIC_FLAG_INIT;
while (flag.test_and_set())
{
// 自旋等待
}
// 临界区
flag.clear();
但普通業務代碼不要因為“看起來無鎖”就自己發明自旋鎖。自旋會持續佔用 CPU,在很多場景下反而不如標準互斥量合適。
9.volatile 不能替代 atomic
這是非常常見的誤區。
volatile bool running;
不能用來解決 C++ 多線程同步問題。
volatile 主要表達的是:
這個對象的訪問具有特殊可觀察意義,編譯器不能像普通對象那樣隨意消除相關訪問。
它不提供:
- 原子性;
- 跨線程同步;
- happens-before 關係;
- 數據競爭保護。
線程間共享狀態應使用 std::atomic、mutex 或其他標準同步工具。
10.內存序:為什麼 atomic 還有第二個參數
你會看到:
value.store(10, std::memory_order_release);
value.load(std::memory_order_acquire);
這個第二個參數控制的是 內存順序(memory ordering)。
原子操作不只解決“這個變量本身是否原子”,還可能承擔不同線程之間建立可見性順序的作用。
C++ 提供的主要內存序包括:
std::memory_order_relaxed
std::memory_order_acquire
std::memory_order_release
std::memory_order_acq_rel
std::memory_order_seq_cst
還有一個歷史上的:
std::memory_order_consume
實際工程中通常不要主動使用 consume。
11.默認內存序:seq_cst
如果不寫第二個參數:
value.store(10);
int x = value.load();
默認使用:
std::memory_order_seq_cst
它提供最直觀、最強的一致性模型,也是最適合初學和默認使用的選擇。
除非你已經能夠證明更弱的內存序是正確且有必要的,否則不要為了“優化”隨便改成 relaxed。
12.memory_order_relaxed
relaxed 只保證這個原子對象本身的操作是原子的,不額外建立其他普通內存訪問的同步順序。
簡單獨立統計計數常常可以使用:
counter.fetch_add(1, std::memory_order_relaxed);
因為我們只關心:
最終計數不要丟。
並不依賴這個計數器來發布其他數據。
13.acquire / release 的基本思想
一個常見模型是:
线程 A 写普通数据
↓
release 写入一个原子标志
↓
线程 B acquire 读到这个标志
↓
线程 B 可以看到此前被正确发布的数据
例如:
#include <atomic>
#include <iostream>
#include <thread>
int data = 0;
std::atomic<bool> ready{false};
void producer()
{
data = 42;
ready.store(true, std::memory_order_release);
}
void consumer()
{
while (!ready.load(std::memory_order_acquire))
{
}
std::cout << data << '\n';
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
}
這裏 data 本身不是 atomic,但 release/acquire 通過 ready 建立了同步關係。
這一部分屬於併發中的進階內容。初學階段最重要的是:
默認先用
seq_cst,只有在明確理解內存模型和性能需求時才主動使用更弱的內存序。
14.is_lock_free()
可以查詢某個原子對象的實現是否 lock-free:
std::atomic<int> value{0};
bool lock_free = value.is_lock_free();
C++20 以後還可以使用類型級常量:
std::atomic<int>::is_always_lock_free
因此不要把:
std::atomic<T>
簡單理解成:
一定由 CPU 的无锁指令实现
它保證的是語義,而不是某一種具體實現方式。
15.C++20:wait() / notify_one() / notify_all()
C++20 為原子對象增加了直接等待值變化的能力。
例如:
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<bool> ready{false};
void worker()
{
ready.wait(false);
std::cout << "start\n";
}
int main()
{
std::thread t(worker);
ready.store(true);
ready.notify_one();
t.join();
}
相比:
while (!ready.load())
{
}
wait() 不需要一直忙等佔用 CPU。
對於“只圍繞一個原子值等待”的簡單場景,這是一種很方便的工具。
16.atomic 和 mutex 怎麼選
| 場景 | 推薦 |
|---|---|
| 簡單計數器 | std::atomic |
| 簡單布爾狀態 | std::atomic<bool> |
| 一個指針或整數的獨立狀態切換 | std::atomic |
| 多個字段必須一起保持一致 | std::mutex |
修改 vector、map 等複雜容器 |
通常用 std::mutex |
| 複雜業務邏輯臨界區 | std::mutex |
| 等待複雜條件 | std::condition_variable + mutex |
不要為了追求“無鎖”而強行把一個本來適合 mutex 的複合狀態拆成很多 atomic。
代碼正確性和可維護性通常比理論上的鎖開銷更重要。
17.常見錯誤
- 把
std::atomic理解成“一定 lock-free”。 - 把
volatile當作線程同步工具。 - 用多個 atomic 表達一個必須整體一致的狀態,卻沒有額外同步。
- 在不理解內存模型時隨意把所有操作改成
memory_order_relaxed。 - 自己實現自旋鎖並在臨界區長時間工作,導致 CPU 空轉。
- 認為“atomic 變量安全”就等於“包含它的整個類都線程安全”。
18.小結
std::atomic<T>保證針對該對象的原子操作語義,但不保證所有實現一定 lock-free。load()/store()負責原子讀寫;fetch_add()等負責原子讀改寫。exchange()可以原子替換並獲得舊值。compare_exchange_weak/strong()是 CAS 操作,也是很多無鎖算法的基礎。volatile不能替代atomic。- 默認內存序是
memory_order_seq_cst,初學階段優先使用默認值。 relaxed、acquire、release屬於更深入的內存模型內容,必須在理解同步關係後再使用。- C++20 的
atomic::wait/notify可以避免簡單狀態等待時的忙輪詢。