Skip to content

Wiki

std::atomic

About 6 min read

An English translation is not available yet. The latest Simplified Chinese source is shown without triggering paid translation.

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 可以避免简单状态等待时的忙轮询。