跳到正文

Wiki

std::atomic

約 6 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

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() 會原子地:

  1. 寫入新值;
  2. 返回舊值。
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::jthreadstd::stop_token,因為它們提供了更結構化的協作式停止機制。

6.atomic 不等於“整個物件執行緒安全”

如果一個類有多個欄位:

std::atomic<int> x;
std::atomic<int> y;

只能保證對 xy 各自的原子操作不會撕裂。

它並不會自動保證:

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.weakstrong

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::atomicmutex 或其他標準同步工具。

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
修改 vectormap 等複雜容器 通常用 std::mutex
複雜業務邏輯臨界區 std::mutex
等待複雜條件 std::condition_variable + mutex

不要為了追求“無鎖”而強行把一個本來適合 mutex 的複合狀態拆成很多 atomic。

程式碼正確性和可維護性通常比理論上的鎖開銷更重要。

17.常見錯誤

  1. std::atomic 理解成“一定 lock-free”。
  2. volatile 當作執行緒同步工具。
  3. 用多個 atomic 表達一個必須整體一致的狀態,卻沒有額外同步。
  4. 在不理解記憶體模型時隨意把所有操作改成 memory_order_relaxed
  5. 自己實現自旋鎖並在臨界區長時間工作,導致 CPU 空轉。
  6. 認為“atomic 變數安全”就等於“包含它的整個類都執行緒安全”。

18.小結

  • std::atomic<T> 保證針對該物件的原子操作語義,但不保證所有實現一定 lock-free。
  • load() / store() 負責原子讀寫;fetch_add() 等負責原子讀改寫。
  • exchange() 可以原子替換並獲得舊值。
  • compare_exchange_weak/strong() 是 CAS 操作,也是很多無鎖演算法的基礎。
  • volatile 不能替代 atomic
  • 預設記憶體序是 memory_order_seq_cst,初學階段優先使用預設值。
  • relaxedacquirerelease 屬於更深入的記憶體模型內容,必須在理解同步關係後再使用。
  • C++20 的 atomic::wait/notify 可以避免簡單狀態等待時的忙輪詢。