Skip to content

Wiki

std::atomic

About 36 min read

Existing English translations were reused. 3 current source blocks remain in Simplified Chinese, without triggering paid translation.

std::atomic<T> Used to perform atomic operations on a shared object, defined in:

#include <atomic>

"Atomic" means that a certain operation cannot be observed as split in half under concurrent observation. For example, with an atomic increment, another thread will not see an intermediate state where "the read has completed but the write-back has not yet completed."

But pay special attention to:

std::atomic Guaranteeing atomic semantics does not mean guaranteeing that the underlying implementation must necessarily lock-free.

Whether it is truly lock-free depends on the type, platform, and standard library implementation.

When learning atomic, do not start by thinking about "lock-free algorithms".

A more beginner-friendly order is:

普通变量为什么会出错
      ↓
单个变量怎样做到原子读写
      ↓
读-改-写怎样作为一个整体完成
      ↓
多个线程之间怎样发布和观察状态
      ↓
什么时候 atomic 不够,需要 mutex

That is to say,std::atomic first and foremost it is acorrectness tool and only secondarily possibly a performance tool.

1.What problem does atomic solve?

1.1.Why ordinary variables are not enough

The following code has a data race:

int counter = 0;

void add_many()
{
    for (int i = 0; i < 100000; ++i)
    {
        ++counter;
    }
}

If multiple threads execute ++counter simultaneously, the result is not reliable.

The reason is not that int itself cannot store numbers, but rather:

++counter;

It looks like a single line of code, but in practice it can usually be split into:

读取 counter
      ↓
加 1
      ↓
写回 counter

When two threads execute interleaved, it is possible for the following to occur:

线程 A 读到 10
线程 B 读到 10
线程 A 写回 11
线程 B 也写回 11

Clearly, the increment was executed twice, but the result only increased by 1.

More importantly, in C++ this is not just "occasionally inaccurate results", but rather a data race and constitutes undefined behavior.

For this "single independent counter" case, you can use:

std::atomic<int> counter{0};

1.2.The most basic load() and store()

Atomic objects can be explicitly read and written:

std::atomic<int> value{0};

value.store(10);
int x = value.load();

Many simple scenarios can also be written in a form similar to ordinary variables:

value = 10;
int x = value;

But when learning atomic semantics, explicitly writing load() / store() makes it easier to see that an atomic access is occurring here.

They can be understood as:

load()  :从 atomic 对象中原子地读出一个值
store() :向 atomic 对象中原子地写入一个值

Here, "atomic" refers only to this single read or write.

For example:

int a = value.load();
int b = other.load();

These two load() are each atomic, but they do not automatically form a single transaction that reads both values at the same time. If you want to read both at the same time, it is still more convenient to use mutex.

1.3.Atomic auto-increment and fetch_add()

自增、自减这类操作叫 读-改-写(read-modify-write, RMW)。

They are not simple reads, nor simple writes, but rather:

读出旧值
      ↓
计算新值
      ↓
写回新值

atomicOne of the key values of is the ability to make thisentire processinto an atomic operation that cannot be interleaved by other threads.

#include <atomic>
#include <print>
#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::println("{}", counter.load());
}

Operation result:

400000

It can also be used explicitly:

counter.fetch_add(1);

Common atomic read-modify-write operations include:

fetch_add()
fetch_sub()
fetch_and()
fetch_or()
fetch_xor()

Among them, the bitwise operation version is mainly used for integer atomic types.

fetch_add()The difference between ++counter and

++counter;

closer to "add one to the value and get the new value after adding";

counter.fetch_add(1);

Express "atomically add 1" more explicitly, and return the old value before the addition.

For example:

int old = counter.fetch_add(1);

If old == 10 this indicates that this operation changed the counter from 10 to 11.

1.4.exchange()

exchange()will atomically:

  1. Write new value;
  2. return old value.
std::atomic<int> value{10};

int old = value.exchange(20);

After execution:

old == 10
value == 20

This kind of "replace and retrieve the old value" operation is very common in state switching.

For example, a task status changes from:

idle

Switch to:

running

If you also want to know the exact state before switching, you can use exchange().

It is better than:

int old = value.load();
value.store(20);

Stronger, because the latter may be modified by other threads inserting in between.

1.5.std::atomic<bool> : Status flag

#include <atomic>
#include <chrono>
#include <print>
#include <thread>

std::atomic<bool> running{true};

void worker()
{
    while (running.load())
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    std::println("stopped");
}

int main()
{
    std::thread t(worker);

    std::this_thread::sleep_for(std::chrono::milliseconds(350));
    running.store(false);

    t.join();
}

Operation result:

stopped

This simple stop flag is well suited to atomic variables.

Here, running expresses only one independent fact:

线程是否继续循环

It does not need to maintain complex invariants together with multiple other fields, so using atomic<bool> is very appropriate.

If, when stopping, many pieces of shared state also need to be modified simultaneously, for example:

running = false
queue.clear()
error_code = xxx

Therefore, one cannot assume that the entire shutdown process is automatically safe just because one of the fields is atomic. In this case, it is usually necessary to reconsider mutex or a more complete shutdown protocol.

mutexA large critical section can be created, whileatomiccannot, so for this kind that requires several lines of critical section, usemutex.

However, if using C++20, you should also understand the following std::jthread and std::stop_token, as they provide a more structured cooperative stopping mechanism.

1.6.atomic is not equal to "the entire object is thread-safe"

If a class has multiple fields:

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

It can only guarantee that the individual atomic operations on x and y will not be torn.

It does not automatically guarantee:

x 和 y 永远表示同一个逻辑状态

For example:

x.store(10);
y.store(20);

Another thread may well observe between the two writes:

x == 10
y == 旧值

If multiple variables must be kept consistent as a whole, the same mutex should usually be used to protect the entire state.

You can remember it in one sentence:

What atomic protects is "a single operation on a single object," whereas a mutex is better suited to protecting "the invariants of a group of states."

For example, a bank account may simultaneously contain:

balance
last_update_time
version

If these three values must change together, then making them individually atomic does not necessarily make the design clearer.

2.CAS and atomic flags

2.1.compare_exchange:Compare and Swap (CAS)

CAS is the core operation of many lock-free algorithms.

However, when you are first learning, do not think of it as too mysterious at the start.

The most core semantics of CAS are:

Only when the value is still the old value I expected should it be changed to the new value.

Conceptually, what it does is:

如果 atomic 当前值 == expected
    atomic = desired
    返回成功
否则
    expected = atomic 当前值
    返回失败

C++ Provided by:

compare_exchange_weak()
compare_exchange_strong()

Example:

#include <atomic>
#include <print>

int main()
{
    std::atomic<int> value{10};

    int expected = 10;
    bool success = value.compare_exchange_strong(expected, 20);

    std::println("{}", success);
    std::println("{}", value.load());
}

Operation result:

true
20

If expected does not match:

std::atomic<int> value{10};
int expected = 5;

bool success = value.compare_exchange_strong(expected, 20);

Then:

success == false
value 仍然是 10
expected 被更新成 10

Here expected will be rewritten; this is a very important detail of CAS.

It is not simply an "input parameter"; when it fails, it will tell you:

你猜的旧值不对,当前真实值是这个

Therefore, the CAS loop is usually written as:

先读当前值作为 expected
      ↓
根据 expected 算 desired
      ↓
尝试 compare_exchange
      ↓
失败时 expected 已被更新,重新计算 desired

2.1.1.weak and strong

compare_exchange_weak() Allow “false failure”: even if the values are equal, failure may still be returned.

Therefore it is well suited for use in loops:

int expected = value.load();

while (!value.compare_exchange_weak(expected, expected + 1))
{
}

This code works because on failure, expected is updated to the current value of atomic.

In the next cycle:

expected + 1

it will continue attempting based on the updated value.

compare_exchange_strong()This kind of pseudo-failure is not allowed, and it is more suitable for scenarios where only one attempt is made.

Simple notation:

循环里反复尝试
→ weak 常见

只想尝试一次
→ strong 更直观

2.2.std::atomic_flag

std::atomic_flagIt can be understood as a very simpleatomic switch.

It has only two states:

false:空闲
true :已占用

Its two common operations are:

test_and_set()
clear()

The most critical among them is:

flag.test_and_set();

This operation willatomically complete two things:

  1. Returns flag the original value;
  2. At the same time, set flag to true.

For example:

std::atomic_flag flag = ATOMIC_FLAG_INIT;

Initially, it can be understood as:

flag = false

First execution:

bool old = flag.test_and_set();

The result is equivalent to:

old = false
flag = true

If at this time another thread executes again:

bool old = flag.test_and_set();

Then the result is:

old = true
flag = true

Therefore, it can be used to implement the simplest spinlock:

std::atomic_flag flag = ATOMIC_FLAG_INIT;

while (flag.test_and_set())
{
    // 自旋等待
}

// 临界区

flag.clear();

The logic of this code is:

flag == false
    ↓
线程执行 test_and_set()
    ↓
返回 false,同时把 flag 改为 true
    ↓
while(false)
    ↓
成功进入临界区

If another thread also executes at this time:

flag.test_and_set();

Since flag is already true it will return true:

while (true)
{
}

Thus this thread will repeatedly execute:

flag.test_and_set();
flag.test_and_set();
flag.test_and_set();
...

Until the thread holding the lock executes:

flag.clear();

Handle:

flag = true

Change back to:

flag = false

When the waiting thread executes next test_and_set(), it can successfully acquire this "lock".

This behavior of continuously looping to check whether the lock has been released is called:

自旋(spin)

It can be understood as:

After a thread discovers that a lock has been acquired by someone else, it neither sleeps nor blocks, but keeps standing there repeatedly asking: "Has the lock been released?"

So one of the differences between spinlocks and ordinary std::mutex lies here.

With an ordinary mutex, when the wait time is relatively long, the thread can usually be blocked or suspended by the operating system, avoiding continuous CPU consumption.

And spin lock:

while (flag.test_and_set())
{
}

It will execute the loop continuously, and therefore may continuously occupy the CPU.

For example:

while (flag.test_and_set())
{
}

// 临界区

read_file();
send_network();
sleep();
do_heavy_work();

flag.clear();

If the critical section takes a long time to execute, other threads will keep spinning and waiting outside, wasting CPU cycles in vain.

Therefore, spinlocks are more suitable for cases where the critical section is very short, for example:

while (flag.test_and_set())
{
}

++value;

flag.clear();

If the lock is going to be released very soon, then having the thread spin a few times may have lower overhead than having the operating system suspend the thread and then wake it up again.

However, in ordinary business code, generally do not use std::atomic_flag to implement locks yourself just because a spinlock "looks simple".

If the following exists in the critical section:

IO

sleep

网络请求

文件操作

复杂计算

可能阻塞的函数

It is usually more appropriate to use:

std::mutex

Coordination:

std::lock_guard
std::scoped_lock
std::unique_lock

for synchronization.

std::atomic_flag What really matters is:

test_and_set()

in:

读取旧值 + 设置为 true

is an indivisible atomic operation.

If using ordinary bool:

bool flag = false;

Two threads may occur simultaneously:

线程 A:看到 flag == false
线程 B:也看到 flag == false

线程 A:flag = true
线程 B:flag = true

线程 A:认为自己获得了锁
线程 B:也认为自己获得了锁

In this way, two threads may enter the critical section simultaneously.

whereas std::atomic_flag::test_and_set() can ensure that when multiple threads compete simultaneously, only one thread can see that the original value is false and thereby successfully enter the critical section.

Therefore, std::atomic_flag can be simply understood as:

std::atomic_flag is an atomic “occupancy flag”.test_and_set() atomically reads the old value and sets the flag to true, so it can be used to implement the simplest spinlock; a thread that fails to acquire the lock will keep looping to check, and this behavior is called spinning.

2.3.volatile cannot replace atomic

This is a very common misconception.

volatile bool running;

Cannotbe used to solve C++ multithread synchronization problems.

volatile The main expression is:

Accesses to this object have special observable semantics, and the compiler cannot arbitrarily eliminate the related accesses as it would for ordinary objects.

It does not provide:

  • Atomicity;
  • Cross-thread synchronization;
  • happens-before relationships;
  • Data race protection.

Shared state between threads should use std::atomic, mutex, or other standard synchronization primitives.

It can be roughly remembered as:

volatile:主要解决“编译器不要省略这次访问”
atomic  :解决“多线程下这次访问如何正确同步”

They are not the same type of tool.

3.Memory ordering

3.1.Why does atomic have a second parameter?

You will see:

value.store(10, std::memory_order_release);

int x = value.load(std::memory_order_acquire);

The second parameter here controls:

Memory ordering

When learning about memory ordering, the most important thing is to first distinguish between two concepts:

原子性

和

内存访问之间的顺序

Atomicity addresses:

Are the operations on this atomic object itself indivisible?

For example:

counter.fetch_add(1);

When multiple threads execute simultaneously, the count will not be lost due to ordinary "read → increment → write back" operations overwriting each other.

What memory ordering addresses is:

What ordering relationships should the other memory reads and writes before and after this atomic operation establish with other threads?

For example:

int data = 0;
std::atomic<bool> ready{false};

Thread A:

data = 42;
ready.store(true, ...);

Thread B:

while (!ready.load(...))
{
}

std::println("{}", data);

Here we are not only concerned with:

ready 能不能正确读到 true

Also concerned about:

线程 B 看到 ready == true 以后,

能不能保证看到线程 A 之前写好的 data = 42。

This is the problem that memory ordering is meant to solve.

C++ Common memory orderings include:

std::memory_order_relaxed
std::memory_order_acquire
std::memory_order_release
std::memory_order_acq_rel
std::memory_order_seq_cst

Also:

std::memory_order_consume

However, in actual engineering practice, it is usually not advisable to actively use consume.


3.2.Default memory order:seq_cst

If the second parameter is not written:

value.store(10);

int x = value.load();

Default use:

std::memory_order_seq_cst

It is the strongest and easiest-to-understand commonly used memory order.

It can first be roughly understood as:

All threads see these atomic operations in an effect as much as possible as if they were all in a globally agreed-upon order.

Therefore, at the beginner stage:

不知道应该用什么
→ 直接使用默认的 seq_cst

It is usually the safest choice.

Do not, for the sake of so-called “performance optimization,” look at:

std::memory_order_relaxed

Just change all atomic operations to relaxed.

If the memory ordering is written incorrectly, the program may run completely normally most of the time, but only after switching platforms, compilers, or optimization levels will very hard-to-diagnose concurrency issues appear.


3.3.memory_order_relaxed

relaxed Only requirement:

Operations on this atomic object itself are still atomic.

For example:

counter.fetch_add(
    1,
    std::memory_order_relaxed);

When multiple threads execute simultaneously, the count will still not be lost due to concurrent updates.

So something like this:

独立统计次数

请求计数

事件发生次数

Usually, one may consider relaxed.

Because we only care about:

counter 自己最后是多少

There is no need to synchronize other ordinary variables through this counter.

But note:

relaxedIt only guarantees atomic's own access; it will not incidentally make nearby ordinary variables thread-safe.

For example:

int data = 0;
std::atomic<bool> ready{false};

void producer()
{
    data = 42;

    ready.store(
        true,
        std::memory_order_relaxed);
}

Even if another thread:

ready.load(std::memory_order_relaxed)

Read to true, and cannot rely solely on this relaxed operation to establish:

data = 42

和

读取 data

the required cross-thread synchronization relationship between them.


3.4.acquire / release

acquire / release The most classic use is:

One thread first prepares data, then publishes via atomic that "the data is ready"; another thread, after seeing this signal, then reads the data.

For example:

#include <atomic>
#include <print>
#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::println("{}", data);
}

int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);

    t1.join();
    t2.join();
}

Output result:

42

It can be understood as follows:

producer:

data = 42
↓
先把普通数据准备好
↓
release store
↓
发布 ready = true

consumer:

acquire load
↓
看到 ready == true
↓
接收到 producer 发布的数据
↓
读取 data

That is:

写普通数据

      ↓

release

      ↓

acquire

      ↓

读普通数据

Here ready itself is only a synchronization signal.

What really matters is:

Operations completed by the producer before release can establish a synchronization relationship with the consumer through the corresponding acquire.

So it can be simply remembered as:

release
→ 发布之前完成的工作

acquire
→ 接收对方发布的工作

3.5.memory_order_acq_rel

Some atomic operations both read the old value and write a new value, for example:

fetch_add()
exchange()
compare_exchange()

This operation sometimes requires simultaneously:

acquire

+

release

Thus, we can use:

std::memory_order_acq_rel

You can first simply understand it as:

既接收别人之前发布的数据

又把自己之前完成的操作继续发布出去

This part appears more often in complex lock-free data structures; at the beginner stage, it is sufficient to know its meaning.


The most important thing to remember is:

原子性
→ atomic 自己的操作是否不可分割

内存序
→ atomic 操作和其他内存访问之间建立怎样的跨线程顺序

and a practical principle:

By default, use seq_cst first; only when the synchronization relationship is clearly understood and it is truly necessary, actively use relaxed , acquire , release and other weaker memory orders.

4.Waiting and Implementation Features

4.1.is_lock_free()

std::atomic<T> What is guaranteed is:

Atomic operations on this object have thread-safety semantics specified by the standard.

However, itdoes not guarantee that the underlying implementation necessarily avoids locks entirely.

For example:

std::atomic<int> value{0};

bool lock_free = value.is_lock_free();

is_lock_free() Used to query:

For the current atomic object, on the current platform and in the current implementation, whether the underlying operation can be completed without relying on additional locks.

If returned:

true

It indicates that its atomic operations are lock-free, meaning that at the lower level they can usually be completed directly by relying on atomic instructions provided by the CPU.

If returned:

false

Note that to implement this atomic operation, the standard library may internally rely on some kind of lock or other synchronization mechanism.

For example:

std::atomic<int> value{0};

std::println("{}", value.is_lock_free());

On common platforms such as x86-64 and ARM, something like:

std::atomic<int>
std::atomic<bool>
std::atomic<int*>

This smaller type is usually lock-free.

However, for relatively large types, the situation is not necessarily the same.

For example:

struct Data
{
    long long a;
    long long b;
};

std::atomic<Data> data;

Such objects are relatively large, and the CPU may not be able to complete the operation directly with one or a small number of atomic instructions.

At this point, the standard library can still provide:

data.load();
data.store(...);

such atomic semantics, but internally it may need to be implemented through locks.

Therefore:

std::atomic<T>

It should be understood as:

Guarantee "the operation is atomic".

rather than:

Guarantee "lock-free implementation to a certain extent".


You can also use:

std::atomic<int>::is_always_lock_free

It is a compile-time constant used to determine:

Is this std::atomic<int> type always lock-free in the current implementation?

For example:

if constexpr (std::atomic<int>::is_always_lock_free)
{
    // 当前实现保证 atomic<int> 永远是 lock-free
}

It can be simply divided into:

value.is_lock_free()

Indicates:

Is this specific atomic object now lock-free?

And:

std::atomic<T>::is_always_lock_free

Indicates:

Does this atomic type always guarantee lock-free in the current implementation.

So the most important point is:

atomic ≠ 一定无锁

atomicWhat is truly expressed is:

原子操作语义

That is, when multiple threads access simultaneously, these operations will not be split in half and allow other threads to interleave.

As for what exactly is at the bottom:

CPU 原子指令

Or:

标准库内部借助锁实现

This is determined by the specific platform and standard library implementation.

Therefore, it can be summarized in one sentence:

std::atomic<T> What is guaranteed is "atomicity", not "definitely lock-free";is_lock_free() it is used to query whether its underlying implementation is truly lock-free.

4.2.C++20:wait() / notify_one() / notify_all()

C++20 Adds to the atomic type:

wait()
notify_one()
notify_all()

They allow a thread to wait for the value of an atomic variable to change without having to write a loop and busy-wait on its own.

For example:

#include <atomic>
#include <print>
#include <thread>

std::atomic<bool> ready{false};

void worker()
{
    ready.wait(false);

    std::println("start");
}

int main()
{
    std::thread t(worker);

    ready.store(true);
    ready.notify_one();

    t.join();
}

Output result:

start

The most critical point here is:

ready.wait(false);

It meansnot:

等待 ready 变成 false

Instead:

如果 ready 当前仍然等于 false,就等待;直到它不再等于 false 才返回。

Therefore, the execution process can be understood as:

初始:

ready = false

workerExecution:

ready.wait(false);

Discovery:

ready == false

that is, the current value and the old value passed in false are the same, so it starts waiting.

Then main Execute:

ready.store(true);

Handle:

ready = false

Change to:

ready = true

Then:

ready.notify_one();

Notify a thread that is waiting for ready :

This value may have changed; you can check it again.

worker After being awakened, it was discovered that:

ready != false

Therefore:

ready.wait(false);

Return and continue executing downward:

std::println("start");

So the entire process can be drawn as:

worker                         main
   |                            |
   | ready.wait(false)          |
   |                            |
   | ready == false             |
   | 开始等待                   |
   |                            |
   |                    ready.store(true)
   |                            |
   |                    ready.notify_one()
   |                            |
   | <------ 被唤醒 ------------|
   |
   | ready != false
   |
   | wait() 返回
   |
   | println("start")

If not using wait() previously it might have been written as:

while (!ready.load())
{
}

This loop will execute continuously:

ready.load();
ready.load();
ready.load();
ready.load();
...

That is, the thread keeps occupying the CPU to check:

become true?

This approach is calledbusy waiting / spin waiting.

And:

ready.wait(false);

When a relatively long wait is required, the thread can be made to block and wait, without needing to spin continuously and consume CPU.

Therefore:

while (!ready.load())
{
}

It can be understood as:

I kept staring at the door to see if it would open.

And:

ready.wait(false);

More like:

If the door hasn't opened, I'll go to sleep first; when someone calls me, I'll get up and take a look.


4.3.notify_one() and notify_all()

If there is only one waiting thread, you can:

ready.notify_one();

Indicates:

Wake up a thread waiting on this atomic variable.

If there are multiple threads:

void worker()
{
    ready.wait(false);
    std::println("start");
}

Then create:

std::thread t1(worker);
std::thread t2(worker);
std::thread t3(worker);

If you want all threads to be awakened, you can:

ready.store(true);
ready.notify_all();

That is:

notify_one()  → 通知一个等待线程

notify_all()  → 通知所有等待线程

4.4.wait() What exactly are we waiting for?

Just remember this rule and it's enough:

atomic.wait(old_value);

Meaning:

As long as the current value is still equal to old_value, continue waiting; once the current value is not equal to old_value , return.

For example:

std::atomic<int> value{10};

value.wait(10);

Indicates:

value == 10
    ↓
继续等待

If another thread executes:

value.store(20);
value.notify_one();

Then:

value != 10

Thus wait(10) returns.

Therefore:

wait(x)

Do not understand it as:

Wait until the value becomes x

Instead, it should be understood as:

While the value is still x , wait; once it is no longer x , continue.


4.5.What is the difference between condition_variable and ? (Come back to this after learning condition_variable)

If your requirements are very simple:

ready 从 false 变成 true

state 从 0 变成其他值

counter 不再等于某个旧值

Direct:

atomic.wait()
atomic.notify_one()
atomic.notify_all()

It will be very convenient.

However, if your wait condition is relatively complex, for example:

队列非空
或者 finished == true
或者 error != 0

This condition involves multiple variables and shared data:

queue.empty()
finished
error

Usually, it is still used:

std::condition_variable
+
std::mutex
+
谓词

Clearer.

Therefore, it can simply be remembered as:

atomic::wait() Suitable for waiting for "the value of an atomic variable to change";condition_variable More suitable for waiting for "a complex condition to become true".

Finally, just remember the core of wait():

value.wait(x);

Equivalent to plain language:

“As long as value is still x, I will keep waiting; whenever it is no longer x, that is when I will continue execution.”

5.Selection and Common Errors

5.1.How to choose between atomic and mutex

Scenario Recommendation
Simple counter std::atomic
Simple boolean state std::atomic<bool>
Independent state toggle of a pointer or integer std::atomic
Multiple fields must be kept consistent together std::mutex
Modify vector, map and other complex containers Usually use std::mutex
Complex business logic critical section std::mutex
Wait for complex conditions std::condition_variable + mutex

Do not force a composite state that is naturally suited to a mutex into many atomics just to pursue "lock-free."

Code correctness and maintainability are usually more important than theoretical lock overhead.

A practical criterion is:

能用一句话说清楚“这个 atomic 独立表示什么状态”
→ 可以考虑 atomic

需要解释好几个字段之间必须如何配合
→ 通常先考虑 mutex

5.2.Common errors

  1. Treat std::atomic as "must be lock-free".
  2. Treat volatile as a thread synchronization tool.
  3. Use multiple atomics to represent a state that must be consistent as a whole, without additional synchronization.
  4. When not understanding the memory model, arbitrarily change all operations to memory_order_relaxed.
  5. Implement a spinlock yourself and work for a long time in the critical section, causing the CPU to spin idle.
  6. Believe that "atomic variables are safe" means "the entire class containing them is thread-safe".

6.Summary

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