命名空间
变体
操作

std::memory_order

来自 cppreference.cn
< cpp‎ | atomic
 
 
并发支持库
线程
(C++11)
(C++20)
this_thread 命名空间
(C++11)
(C++11)
(C++11)
协作取消
互斥
(C++11)
通用锁管理
(C++11)
(C++11)
(C++11)
(C++11)
(C++11)
条件变量
(C++11)
信号量
闩锁和屏障
(C++20)
(C++20)
期物
(C++11)
(C++11)
(C++11)
(C++11)
安全回收
(C++26)
Hazard 指针
原子类型
(C++11)
(C++20)
原子类型的初始化
(C++11)(在 C++20 中弃用)
(C++11)(在 C++20 中弃用)
内存顺序
memory_order
(C++11)
(C++11)(在 C++26 中弃用)
原子操作的自由函数
原子标志的自由函数
 
定义于头文件 <atomic>
enum memory_order

{
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst

};
(自 C++11 起)
(直至 C++20)
enum class memory_order : /* 未指定 */

{
    relaxed, consume, acquire, release, acq_rel, seq_cst
};
inline constexpr memory_order memory_order_relaxed = memory_order::relaxed;
inline constexpr memory_order memory_order_consume = memory_order::consume;
inline constexpr memory_order memory_order_acquire = memory_order::acquire;
inline constexpr memory_order memory_order_release = memory_order::release;
inline constexpr memory_order memory_order_acq_rel = memory_order::acq_rel;

inline constexpr memory_order memory_order_seq_cst = memory_order::seq_cst;
(自 C++20 起)

std::memory_order 指定了内存访问,包括常规的非原子内存访问,在原子操作周围的排序方式。在多核系统上,如果没有约束,当多个线程同时读写多个变量时,一个线程可以观察到值的变化顺序与另一个线程写入它们的顺序不同。实际上,对于多个读取线程,更改的明显顺序甚至可能不同。由于内存模型允许的编译器转换,即使在单处理器系统上,也可能发生一些类似的效果。

库中所有原子操作的默认行为都提供了顺序一致性排序(见下文讨论)。这种默认设置可能会损害性能,但库的原子操作可以获得额外的 std::memory_order 参数,以指定编译器和处理器必须为该操作强制执行的超出原子性的精确约束。

内容

[编辑] 常量

定义于头文件 <atomic>
名称 解释
memory_order_relaxed 宽松操作:对其他读取或写入没有施加同步或排序约束,仅保证此操作的原子性(见下文宽松排序)。
memory_order_consume
(在 C++26 中弃用)
具有此内存顺序的加载操作对受影响的内存位置执行消费操作:当前线程中依赖于当前加载的值的任何读取或写入都不能在此加载之前重新排序。在其他线程中释放同一原子变量的数据依赖变量的写入在当前线程中可见。在大多数平台上,这仅影响编译器优化(见下文释放-消费排序)。
memory_order_acquire 具有此内存顺序的加载操作对受影响的内存位置执行获取操作:当前线程中的任何读取或写入都不能在此加载之前重新排序。在其他线程中释放同一原子变量的所有写入在当前线程中可见(见下文释放-获取排序)。
memory_order_release 具有此内存顺序的存储操作执行释放操作:当前线程中的任何读取或写入都不能在此存储之后重新排序。当前线程中的所有写入在其他线程中获取同一原子变量时都可见(见下文释放-获取排序),并且携带依赖到原子变量中的写入在其他线程消费同一原子变量时变得可见(见下文释放-消费排序)。
memory_order_acq_rel 具有此内存顺序的读-修改-写操作既是获取操作又是释放操作。当前线程中没有内存读取或写入可以在加载之前重新排序,也没有可以在存储之后重新排序。在修改之前,在其他线程中释放同一原子变量的所有写入都是可见的,并且修改在其他线程中获取同一原子变量时是可见的。
memory_order_seq_cst 具有此内存顺序的加载操作执行获取操作,存储操作执行释放操作,读-修改-写操作既执行获取操作又执行释放操作,此外,还存在一个单一的总顺序,其中所有线程以相同的顺序观察所有修改(见下文顺序一致性排序)。

[编辑] 形式描述

线程间同步和内存顺序确定了表达式的求值副作用在不同执行线程之间的排序方式。它们以下列术语定义:

[编辑] 先序发生

在同一线程内,求值 A 可以先序发生于求值 B,如求值顺序中所述。

携带依赖

在同一线程内,先序发生于求值 B 的求值 A 也可能将依赖携带到 B 中(即,B 依赖于 A),如果以下任何一项为真:

1) A 的值用作 B 的操作数,除非
a) 如果 B 是对 std::kill_dependency 的调用,
b) 如果 A 是内置 &&||?:, 运算符的左操作数。
2) A 写入标量对象 M,B 从 M 读取。
3) A 将依赖携带到另一个求值 X,并且 X 将依赖携带到 B。
(直至 C++26)

[编辑] 修改顺序

对任何特定原子变量的所有修改都以特定于此原子变量的总顺序发生。

以下四个要求保证适用于所有原子操作:

1) 写-写一致性:如果修改某个原子 M 的求值 A(写入)先序发生于修改 M 的求值 B,则 A 在 M 的修改顺序中出现在 B 之前。
2) 读-读一致性:如果对某个原子 M 的值计算 A(读取)先序发生于对 M 的值计算 B,并且如果 A 的值来自对 M 的写入 X,则 B 的值是 X 存储的值,或者是对 M 的副作用 Y 存储的值,而 Y 在 M 的修改顺序中出现在 X 之后。
3) 读-写一致性:如果对某个原子 M 的值计算 A(读取)先序发生于对 M 的操作 B(写入),则 A 的值来自在 M 的修改顺序中早于 B 出现的副作用(写入)X。
4) 写-读一致性:如果对原子对象 M 的副作用(写入)X 先序发生于对 M 的值计算(读取)B,则求值 B 应从 X 或在 M 的修改顺序中跟随 X 的副作用 Y 中获取其值。

[编辑] 释放序列

在原子对象 M 上执行释放操作 A 之后,M 的修改顺序的最长连续子序列,该子序列由以下各项组成:

1) 由执行 A 的同一线程执行的写入。
(直至 C++20)
2) 由任何线程对 M 进行的原子读-修改-写操作。

被称为以 A 为首的释放序列

[编辑] 同步于

如果线程 A 中的原子存储是释放操作,线程 B 中从同一变量进行的原子加载是获取操作,并且线程 B 中的加载读取了线程 A 中的存储写入的值,则线程 A 中的存储同步于线程 B 中的加载。

此外,一些库调用可能被定义为同步于其他线程上的其他库调用。

依赖顺序先于

在线程之间,如果以下任何一项为真,则求值 A 依赖顺序先于求值 B:

1) A 对某个原子 M 执行释放操作,并且在不同的线程中,B 对同一原子 M 执行消费操作,并且 B 读取了由以 A 为首的释放序列的任何部分写入的值(直至 C++20)
2) A 依赖顺序先于 X,并且 X 将依赖携带到 B。
(直至 C++26)

[编辑] 线程间先序发生

在线程之间,如果以下任何一项为真,则求值 A 线程间先序发生于求值 B:

1) A 同步于 B。
2) A 依赖顺序先于 B。
3) A 同步于某个求值 X,并且 X 先序发生于 B。
4) A 先序发生于某个求值 X,并且 X 线程间先序发生于 B。
5) A 线程间先序发生于某个求值 X,并且 X 线程间先序发生于 B。


先序发生

无论线程如何,如果以下任何一项为真,则求值 A 先序发生于求值 B:

1) A 先序发生于 B。
2) A 线程间先序发生于 B。

实现需要确保先序发生关系是非循环的,如有必要,通过引入额外的同步(只有在涉及消费操作时才可能是必要的,参见 Batty et al)。

如果一个求值修改了一个内存位置,而另一个求值读取或修改了相同的内存位置,并且如果至少有一个求值不是原子操作,则程序的行为是未定义的(程序存在数据竞争),除非在这两个求值之间存在先序发生关系。

简单先序发生

无论线程如何,如果以下任何一项为真,则求值 A 简单先序发生于求值 B:

1) A 先序发生于 B。
2) A 同步于 B。
3) A 简单先序发生于 X,并且 X 简单先序发生于 B。

注意:在没有消费操作的情况下,简单先序发生先序发生关系是相同的。

(自 C++20 起)
(直至 C++26)

先序发生

无论线程如何,如果以下任何一项为真,则求值 A 先序发生于求值 B:

1) A 先序发生于 B。
2) A 同步于 B。
3) A 先序发生于 X,并且 X 先序发生于 B。
(自 C++26 起)

[编辑] 强先序发生

无论线程如何,如果以下任何一项为真,则求值 A 强先序发生于求值 B:

1) A 先序发生于 B。
2) A 同步于 B。
3) A 强先序发生于 X,并且 X 强先序发生于 B。
(直至 C++20)
1) A 先序发生于 B。
2) A 同步于 B,并且 A 和 B 都是顺序一致的原子操作。
3) A 先序发生于 X,X 简单(直至 C++26) 先序发生于 Y,并且 Y 先序发生于 B。
4) A 强先序发生于 X,并且 X 强先序发生于 B。

注意:非正式地,如果 A 强先序发生于 B,则在所有上下文中,A 似乎都在 B 之前被求值。

注意:强先序发生排除消费操作。

(直至 C++26)
(自 C++20 起)

[编辑] 可见的副作用

标量 M 上的副作用 A(写入)相对于 M 上的值计算 B(读取)是可见的,如果以下两者都为真:

1) A 先序发生于 B。
2) 没有其他对 M 的副作用 X,其中 A 先序发生于 X 并且 X 先序发生于 B。

如果副作用 A 相对于值计算 B 是可见的,则在修改顺序中,对 M 的副作用的最长连续子集,其中 B 不先序发生于它是已知的可见的副作用序列(由 B 确定的 M 的值将是这些副作用之一存储的值)。

注意:线程间同步归结为防止数据竞争(通过建立先序发生关系)和定义在什么条件下副作用变得可见。

[编辑] 消费操作

具有 memory_order_consume 或更强内存顺序的原子加载是消费操作。请注意,std::atomic_thread_fence 施加的同步要求比消费操作更强。

[编辑] 获取操作

具有 memory_order_acquire 或更强内存顺序的原子加载是获取操作。互斥量上的 lock() 操作也是获取操作。请注意,std::atomic_thread_fence 施加的同步要求比获取操作更强。

[编辑] 释放操作

具有 memory_order_release 或更强内存顺序的原子存储是释放操作。互斥量上的 unlock() 操作也是释放操作。请注意,std::atomic_thread_fence 施加的同步要求比释放操作更强。

[编辑] 解释

[编辑] 宽松排序

标记为 memory_order_relaxed 的原子操作不是同步操作;它们不对并发内存访问施加顺序。它们仅保证原子性和修改顺序一致性。

例如,假设 xy 最初为零,

// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C 
y.store(42, std::memory_order_relaxed); // D

可能产生 r1 == r2 == 42,因为,尽管 A 在线程 1 中先序发生于 B,并且 C 在线程 2 中先序发生于 D,但没有什么可以阻止 D 在 y 的修改顺序中出现在 A 之前,以及 B 在 x 的修改顺序中出现在 C 之前。D 对 y 的副作用可能对线程 1 中的加载 A 可见,而 B 对 x 的副作用可能对线程 2 中的加载 C 可见。特别是,如果 D 在线程 2 中在 C 之前完成,则可能发生这种情况,这可能是由于编译器重新排序或在运行时。

即使使用宽松内存模型,也不允许凭空出现的值循环依赖于它们自己的计算,例如,假设 xy 最初为零,

// Thread 1:
r1 = y.load(std::memory_order_relaxed);
if (r1 == 42)
    x.store(r1, std::memory_order_relaxed);
// Thread 2:
r2 = x.load(std::memory_order_relaxed);
if (r2 == 42)
    y.store(42, std::memory_order_relaxed);

不允许产生 r1 == r2 == 42,因为只有当对 x 的存储存储 42 时,才有可能将 42 存储到 y,这循环依赖于对 y 存储 42。请注意,直到 C++14,这在技术上是被规范允许的,但不建议实现者这样做。

(自 C++14 起)

宽松内存排序的典型用途是递增计数器,例如 std::shared_ptr 的引用计数器,因为这只需要原子性,而不需要排序或同步(请注意,递减 std::shared_ptr 计数器需要与析构函数进行获取-释放同步)。

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
 
std::atomic<int> cnt = {0};
 
void f()
{
    for (int n = 0; n < 1000; ++n)
        cnt.fetch_add(1, std::memory_order_relaxed);
}
 
int main()
{
    std::vector<std::thread> v;
    for (int n = 0; n < 10; ++n)
        v.emplace_back(f);
    for (auto& t : v)
        t.join();
    std::cout << "Final counter value is " << cnt << '\n';
}

输出

Final counter value is 10000

[编辑] 释放-获取排序

如果线程 A 中的原子存储标记为 memory_order_release,线程 B 中从同一变量进行的原子加载标记为 memory_order_acquire,并且线程 B 中的加载读取了线程 A 中的存储写入的值,则线程 A 中的存储同步于线程 B 中的加载。

从线程 A 的角度来看,所有先序发生于原子存储的内存写入(包括非原子和宽松原子)都成为线程 B 中的可见的副作用。也就是说,一旦原子加载完成,就保证线程 B 可以看到线程 A 写入内存的所有内容。只有当 B 实际返回 A 存储的值或释放序列中稍后的值时,此承诺才成立。

同步仅在释放获取同一原子变量的线程之间建立。其他线程可以看到与同步线程中的一个或两个不同的内存访问顺序。

在强排序系统(x86、SPARC TSO、IBM 大型机等)上,释放-获取排序对于大多数操作是自动的。此同步模式不会发出额外的 CPU 指令;仅影响某些编译器优化(例如,编译器被禁止将非原子存储移动到原子存储-释放之后,或执行早于原子加载-获取的非原子加载)。在弱排序系统(ARM、Itanium、PowerPC)上,使用特殊的 CPU 加载或内存栅栏指令。

互斥锁(例如 std::mutex原子自旋锁)是释放-获取同步的一个示例:当线程 A 释放锁并由线程 B 获取锁时,在线程 A 的上下文中,在临界区内发生的所有事情(在释放之前)都必须对线程 B 可见(在获取之后),线程 B 正在执行相同的临界区。

#include <atomic>
#include <cassert>
#include <string>
#include <thread>
 
std::atomic<std::string*> ptr;
int data;
 
void producer()
{
    std::string* p = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}
 
void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_acquire)))
        ;
    assert(*p2 == "Hello"); // never fires
    assert(data == 42); // never fires
}
 
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

以下示例演示了跨三个线程的传递释放-获取排序,使用释放序列。

#include <atomic>
#include <cassert>
#include <thread>
#include <vector>
 
std::vector<int> data;
std::atomic<int> flag = {0};
 
void thread_1()
{
    data.push_back(42);
    flag.store(1, std::memory_order_release);
}
 
void thread_2()
{
    int expected = 1;
    // memory_order_relaxed is okay because this is an RMW,
    // and RMWs (with any ordering) following a release form a release sequence
    while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed))
    {
        expected = 1;
    }
}
 
void thread_3()
{
    while (flag.load(std::memory_order_acquire) < 2)
        ;
    // if we read the value 2 from the atomic flag, we see 42 in the vector
    assert(data.at(0) == 42); // will never fire
}
 
int main()
{
    std::thread a(thread_1);
    std::thread b(thread_2);
    std::thread c(thread_3);
    a.join(); b.join(); c.join();
}

[编辑] 释放-消费排序

如果线程 A 中的原子存储标记为 memory_order_release,线程 B 中从同一变量进行的原子加载标记为 memory_order_consume,并且线程 B 中的加载读取了线程 A 中的存储写入的值,则线程 A 中的存储依赖顺序先于线程 B 中的加载。

从线程 A 的角度来看,所有先序发生于原子存储的内存写入(非原子和宽松原子)都成为线程 B 中携带依赖的那些操作中的可见的副作用,也就是说,一旦原子加载完成,就保证线程 B 中使用从加载操作获得的值的那些运算符和函数可以看到线程 A 写入内存的内容。

同步仅在释放消费同一原子变量的线程之间建立。其他线程可以看到与同步线程中的一个或两个不同的内存访问顺序。

在除 DEC Alpha 之外的所有主流 CPU 上,依赖顺序是自动的,此同步模式不会发出额外的 CPU 指令,仅影响某些编译器优化(例如,编译器被禁止对依赖链中涉及的对象执行推测性加载)。

此排序的典型用例包括对极少写入的并发数据结构(路由表、配置、安全策略、防火墙规则等)的读取访问,以及发布者-订阅者情境中通过指针传递的发布,即当生产者发布一个指针,消费者可以通过该指针访问信息时:无需使生产者写入内存的其他所有内容对消费者可见(这在弱序架构上可能是一项昂贵的操作)。 这种情况的一个例子是 rcu_dereference

另请参阅 std::kill_dependency[[carries_dependency]],用于细粒度的依赖链控制。

请注意,目前(2015 年 2 月)没有已知的生产编译器跟踪依赖链:consume 操作被提升为 acquire 操作。

(直至 C++26)

release-consume 排序的规范正在修订中,暂时不鼓励使用 memory_order_consume

(自 C++17 起)
(直至 C++26)

Release-consume 排序与 release-acquire 排序具有相同的效果,并且已被弃用。

(自 C++26 起)

此示例演示了用于指针传递发布的依赖顺序同步:整数数据与指向字符串的指针之间没有数据依赖关系,因此其值在消费者中是未定义的。

#include <atomic>
#include <cassert>
#include <string>
#include <thread>
 
std::atomic<std::string*> ptr;
int data;
 
void producer()
{
    std::string* p = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}
 
void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_consume)))
        ;
    assert(*p2 == "Hello"); // never fires: *p2 carries dependency from ptr
    assert(data == 42); // may or may not fire: data does not carry dependency from ptr
}
 
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}


[编辑] 顺序一致性排序

标记为 memory_order_seq_cst 的原子操作不仅以与 release/acquire 排序相同的方式对内存进行排序(在一个线程中存储操作之前发生的所有内容都成为执行加载操作的线程中的可见副作用),而且还为所有如此标记的原子操作建立单一的总体修改顺序

形式上,

每个从原子变量 M 加载的 memory_order_seq_cst 操作 B,观察到以下情况之一

  • 最后修改 M 的操作 A 的结果,它在单一的总体顺序中出现在 B 之前,
  • 或者,如果存在这样的 A,B 可能会观察到 M 上的一些修改的结果,该修改不是 memory_order_seq_cst 并且不先于发生 A,
  • 或者,如果不存在这样的 A,B 可能会观察到 M 的一些不相关的修改的结果,该修改不是 memory_order_seq_cst

如果存在 memory_order_seq_cst std::atomic_thread_fence 操作 X 先序于 B,则 B 观察到以下情况之一

  • 在单一的总体顺序中出现在 X 之前的 M 的最后 memory_order_seq_cst 修改,
  • M 的修改顺序中稍后出现的一些不相关的 M 修改。

对于 M 上的一对原子操作 A 和 B,其中 A 写入 M 的值,B 读取 M 的值,如果存在两个 memory_order_seq_cst std::atomic_thread_fence X 和 Y,并且如果 A 先序于 X,Y 先序于 B,并且 X 在单一总体顺序中出现在 Y 之前,则 B 观察到以下情况之一

  • A 的效果,
  • M 的修改顺序中在 A 之后出现的一些不相关的 M 修改。

对于 M 的一对原子修改 A 和 B,如果发生以下情况,则 B 在 M 的修改顺序中发生在 A 之后

  • 存在一个 memory_order_seq_cst std::atomic_thread_fence X,使得 A 先序于 X 并且 X 在单一总体顺序中出现在 B 之前,
  • 或者,存在一个 memory_order_seq_cst std::atomic_thread_fence Y,使得 Y 先序于 B 并且 A 在单一总体顺序中出现在 Y 之前,
  • 或者,存在 memory_order_seq_cst std::atomic_thread_fence X 和 Y,使得 A 先序于 X,Y 先序于 B,并且 X 在单一总体顺序中出现在 Y 之前。

请注意,这意味着

1) 一旦未标记为 memory_order_seq_cst 的原子操作进入场景,顺序一致性就会丢失,
2) 顺序一致性栅栏仅为栅栏本身建立总体顺序,而不是为一般情况下的原子操作建立总体顺序(先序于不是跨线程关系,与先于发生不同)。
(直至 C++20)
形式上,

原子对象 M 上的原子操作 A 一致性顺序先于 M 上的另一个原子操作 B,如果以下任何一项为真

1) A 是一个修改,而 B 读取由 A 存储的值,
2) A 在 M 的修改顺序中先于 B,
3) A 读取由原子修改 X 存储的值,X 在修改顺序中先于 B,并且 A 和 B 不是相同的原子读取-修改-写入操作,
4) A 一致性顺序先于 X,并且 X 一致性顺序先于 B。

所有 memory_order_seq_cst 操作(包括栅栏)都存在一个单一的总体顺序 S,它满足以下约束

1) 如果 A 和 B 是 memory_order_seq_cst 操作,并且 A 强先于发生 B,则 A 在 S 中先于 B,
2) 对于对象 M 上每一对原子操作 A 和 B,其中 A 一致性顺序先于 B
a) 如果 A 和 B 都是 memory_order_seq_cst 操作,则 A 在 S 中先于 B,
b) 如果 A 是 memory_order_seq_cst 操作,并且 B 先于发生 memory_order_seq_cst 栅栏 Y,则 A 在 S 中先于 Y,
c) 如果 memory_order_seq_cst 栅栏 X 先于发生 A,并且 B 是 memory_order_seq_cst 操作,则 X 在 S 中先于 B,
d) 如果 memory_order_seq_cst 栅栏 X 先于发生 A,并且 B 先于发生 memory_order_seq_cst 栅栏 Y,则 X 在 S 中先于 Y。

正式定义确保

1) 单一的总体顺序与任何原子对象的修改顺序一致,
2) memory_order_seq_cst 加载从最后一个 memory_order_seq_cst 修改或一些非 memory_order_seq_cst 修改中获取其值,该修改不先于发生之前的 memory_order_seq_cst 修改。

单一的总体顺序可能与先于发生不一致。 这允许在某些 CPU 上更有效地实现 memory_order_acquirememory_order_release。 当 memory_order_acquirememory_order_releasememory_order_seq_cst 混合使用时,可能会产生令人惊讶的结果。

例如,当 xy 最初为零时,

// Thread 1:
x.store(1, std::memory_order_seq_cst); // A
y.store(1, std::memory_order_release); // B
// Thread 2:
r1 = y.fetch_add(1, std::memory_order_seq_cst); // C
r2 = y.load(std::memory_order_relaxed); // D
// Thread 3:
y.store(3, std::memory_order_seq_cst); // E
r3 = x.load(std::memory_order_seq_cst); // F

允许产生 r1 == 1 && r2 == 3 && r3 == 0,其中 A 先于发生 C,但 C 在 memory_order_seq_cst 的单一总体顺序 C-E-F-A 中先于 A(参见 Lahav et al)。

请注意

1) 一旦未标记为 memory_order_seq_cst 的原子操作进入场景,程序的顺序一致性保证就会丢失,
2) 在许多情况下,memory_order_seq_cst 原子操作可以相对于同一线程执行的其他原子操作重新排序。
(自 C++20 起)

在多生产者-多消费者情况下,可能需要顺序排序,在这种情况下,所有消费者必须以相同的顺序观察所有生产者的操作。

总顺序排序需要在所有多核系统上使用完整的内存栅栏 CPU 指令。 这可能会成为性能瓶颈,因为它会强制受影响的内存访问传播到每个核心。

此示例演示了需要顺序排序的情况。 任何其他排序都可能触发断言,因为线程 cd 可能以相反的顺序观察到对原子变量 xy 的更改。

#include <atomic>
#include <cassert>
#include <thread>
 
std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};
 
void write_x()
{
    x.store(true, std::memory_order_seq_cst);
}
 
void write_y()
{
    y.store(true, std::memory_order_seq_cst);
}
 
void read_x_then_y()
{
    while (!x.load(std::memory_order_seq_cst))
        ;
    if (y.load(std::memory_order_seq_cst))
        ++z;
}
 
void read_y_then_x()
{
    while (!y.load(std::memory_order_seq_cst))
        ;
    if (x.load(std::memory_order_seq_cst))
        ++z;
}
 
int main()
{
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join(); b.join(); c.join(); d.join();
    assert(z.load() != 0); // will never happen
}

[编辑] volatile 的关系

在执行线程中,通过 volatile 左值 的访问(读取和写入)不能在同一线程内先序于后序于可观察的副作用(包括其他 volatile 访问)重新排序,但是不能保证另一个线程会观察到此顺序,因为 volatile 访问不建立线程间同步。

此外,volatile 访问不是原子的(并发读取和写入是数据竞争),并且不对内存进行排序(非 volatile 内存访问可以在 volatile 访问周围自由重新排序)。

一个值得注意的例外是 Visual Studio,在 Visual Studio 中,使用默认设置,每个 volatile 写入都具有 release 语义,每个 volatile 读取都具有 acquire 语义(Microsoft Docs),因此 volatile 可以用于线程间同步。 标准 volatile 语义不适用于多线程编程,尽管它们对于例如与应用于 sig_atomic_t 变量时在同一线程中运行的 std::signal 处理程序通信是足够的。

[编辑] 参见

C 文档 关于 memory order

[编辑] 外部链接

1.  MOESI 协议
2.  x86-TSO: x86 多处理器的严格且可用的程序员模型 P. Sewell 等人,2010
3.  ARM 和 POWER 放松内存模型教程介绍 P. Sewell 等人,2012
4.  MESIF:点对点互连的双跳缓存一致性协议 J.R. Goodman, H.H.J. Hum, 2009
5.  内存模型 Russ Cox, 2021