std::memory_order

来自cppreference.com
< cpp‎ | atomic
 
 
 
定义于头文件 <atomic>
typedef enum memory_order {

    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst

} memory_order;
(C++11 起)
(C++20 前)
enum class memory_order : /*unspecified*/ {

    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 有此内存顺序的加载操作,在其影响的内存位置进行消费操作:当前线程中依赖于当前加载的该值的读或写不能被重排到此加载前。其他释放同一原子变量的线程的对数据依赖变量的写入,为当前线程所可见。在大多数平台上,这只影响到编译器优化(见下方释放消费顺序)。
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

修改顺序

对任何特定的原子变量的修改,以限定于此一原子变量的单独全序出现。

对所有原子操作保证下列四个要求:

1) 写写连贯:若修改某原子对象 M 的求值 A (写操作)先发生于修改 M 的求值 B ,则 A 在 M 的修改顺序中早于 B 出现。
2) 读读连贯:若某原子对象 M 的值计算 A (读操作)先发生于对 M 的值计算 B ,且 A 的值来自对 M 的写操作 X ,则 B 的值要么是 X 所存储的值,要么是在 M 的修改顺序中后于 X 出现的 M 上的副效应 Y 所存储的值。
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

1) A 在某原子对象 M 上进行释放操作,而不同的线程中, B 在同一原子对象 M 上进行消费操作,而 B 读取 A 所引领的释放序列的任何部分 (C++20 前)所写入的值。
2) A 依赖先序于 X 且 X 携带依赖到 B 。

线程间先发生于

在线程间,若下列任一为真,则求值 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 等)。

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

简单先发生于

无关乎线程,若下列之一为真,则求值 A 简单先发生于求值 B :

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

注:不计消费操作,则简单先发生于强先发生于关系是相同的。

(C++20 起)

强先发生于

无关乎线程,若下列之一为真,则求值 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 简单先发生于 Y ,而 Y 先序于 B
4) A 强先发生于 X ,而 X 强先发生于 B

注:非正式而言,若 A 强先发生于 B ,则在所有环境中 A 均显得在 B 之前得到求值。

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

(C++20 起)

可见副效应

若下列皆为真,则标量 M 上的副效应 A (写入)相对于 M 上的值计算(读取)可见

1) A 先发生于 B
2) 没有其他对 M 的副效应 X 满足 A 先发生于 X 且 X 先发生于 B

若副效应 A 相对于值计算 B 可见,则修改顺序中,满足 B 不先发生于它的对 M 的副效应的最长相接子集,被称为副效应的可见序列。( B 所确定的 M 的值,将是这些副效应之一所存储的值)

注意:线程间同步可归结为避免数据竞争(通过建立先发生于关系),及定义在何种条件下哪些副效应成为可见。

消费操作

memory_order_consume 或更强标签的原子加载是消费操作。注意 std::atomic_thread_fence 会施加比消费操作更强的同步要求。

获得操作

memory_order_acquire 或更强标签的原子加载是获得操作。互斥体 (Mutex) 上的 lock() 操作亦为获得操作。注意 std::atomic_thread_fence 会施加比获得操作更强的同步要求。

释放操作

memory_order_release 或更强标签的原子存储是释放操作。互斥体 (Mutex) 上的 unlock() 操作亦为释放操作。注意 std::atomic_thread_fence 会施加比释放操作更强的同步要求。

解释

宽松顺序

带标签 memory_order_relaxed 的原子操作无同步操作;它们不会在共时的内存访问间强加顺序。它们只保证原子性和修改顺序一致性。

例如,对于最初为零的 xy

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

允许产生结果 r1 == 42 && r2 == 42 ,因为即使线程 1 中 A 先序于 B 且线程 2 中 C 先序于 D ,却没有制约避免 y 的修改顺序中 D 先出现于 A ,而 x 的修改顺序中 B 先出现于 C 。 D 在 y 上的副效应,可能可见于线程 1 中的加载 A ,同时 B 在 x 上的副效应,可能可见于线程 2 中的加载 C 。

即使使用宽松内存模型,也不允许“无中生有”的值循环地依赖于其各自的计算,例如,对于最初为零的 xy

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

不允许产生结果 r1 == 42 && r2 == 42 ,因为存储 42 于 y 只在存储 42 于 x 后有可能,这又循环依赖于存储 42 于 y 。注意 C++14 前规范曾在技术上允许,但不推荐实现者如此实现。

(C++14 起)

宽松内存顺序的典型使用是计数器自增,例如 std::shared_ptr 的引用计数器,因为这只要求原子性,但不要求顺序或同步(注意 std::shared_ptr 计数器自减要求与析构函数进行获得释放同步)

#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
 
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 ,则从线程 A 的视角先发生于原子存储的所有内存写入(非原子及宽松原子的),在线程 B 中成为可见副效应,即一旦原子加载完成,则保证线程 B 能观察到线程 A 写入内存的所有内容。

同步仅建立在释放获得同一原子对象的线程之间。其他线程可能看到与被同步线程的一者或两者相异的内存访问顺序。

在强顺序系统( x86 、 SPARC TSO 、 IBM 主框架)上,释放获得顺序对于多数操作是自动进行的。无需为此同步模式添加额外的 CPU 指令,只有某些编译器优化受影响(例如,编译器被禁止将非原子存储移到原子存储-释放后,或将非原子加载移到原子加载-获得前)。在弱顺序系统( ARM 、 Itanium 、 Power PC )上,必须使用特别的 CPU 加载或内存栅栏指令。

互斥锁(例如 std::mutex原子自旋锁)是释放获得同步的例子:线程 A 释放锁而线程 B 获得它时,发生于线程 A 环境的临界区(释放之前)中的所有事件,必须对于执行同一临界区的线程 B (获得之后)可见。

#include <thread>
#include <atomic>
#include <cassert>
#include <string>
 
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"); // 绝无问题
    assert(data == 42); // 绝无问题
}
 
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

下例演示三个线程间传递性的释放获得顺序

#include <thread>
#include <atomic>
#include <cassert>
#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;
    while (!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) {
        expected = 1;
    }
}
 
void thread_3()
{
    while (flag.load(std::memory_order_acquire) < 2)
        ;
    assert(data.at(0) == 42); // 决不出错
}
 
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 ,则线程 A 视角中先发生于原子存储的所有内存写入(非原子和宽松原子的),会在线程 B 中该加载操作所携带依赖进入的操作中变成可见副效应,即一旦完成原子加载,则保证线程B中,使用从该加载获得的值的运算符和函数,能见到线程 A 写入内存的内容。

同步仅在释放消费同一原子对象的线程间建立。其他线程能见到与被同步线程的一者或两者相异的内存访问顺序。

所有异于 DEC Alpha 的主流 CPU 上,依赖顺序是自动的,无需为此同步模式产生附加的 CPU 指令,只有某些编译器优化收益受影响(例如,编译器被禁止牵涉到依赖链的对象上的推测性加载)。

此顺序的典型使用情况,涉及对很少被写入的数据结构(安排表、配置、安全策略、防火墙规则等)的共时读取,和有指针中介发布的发布者-订阅者情形,即当生产者发布消费者能通过其访问信息的指针之时:无需令生产者写入内存的所有其他内容对消费者可见(这在弱顺序架构上可能是昂贵的操作)。这种场景的例子之一是 rcu 解引用

细粒度依赖链控制可参阅 std::kill_dependency[[carries_dependency]]

注意到 2015 年 2 月为止没有产品编译器跟踪依赖链:均将消费操作提升为获得操作。

释放消费顺序的规范正在修订中,而且暂时不鼓励使用 memory_order_consume

(C++17 起)

此示例演示用于指针中介的发布的依赖定序同步:int data不由数据依赖关系关联到指向字符串的指针,从而其值在消费者中未定义。

#include <thread>
#include <atomic>
#include <cassert>
#include <string>
 
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"); // 绝无出错: *p2 从 ptr 携带依赖
    assert(data == 42); // 可能也可能不会出错: data 不从 ptr 携带依赖
}
 
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}


序列一致顺序

带标签 memory_order_seq_cst 的原子操作不仅以与释放/获得顺序相同的方式排序内存(在一个线程中先发生于存储的任何结果都变成进行加载的线程中的可见副效应),还对所有带此标签的内存操作建立单独全序

正式而言,

每个加载原子对象 M 的 memory_order_seq_cst 操作 B ,观测到以下之一:

  • 修改 M 的上个操作 A 的结果,A 在单独全序中先出现于 B
  • 或若存在这种 A ,则 B 可能观测到某些 M 的修改结果,这些修改非 memory_order_seq_cst 而且不先发生于 A
  • 或若不存在这种 A ,则 B 可能观测到某些 M 的无关联修改,这些修改非 memory_order_seq_cst

若存在 memory_order_seq_cststd::atomic_thread_fence 操作 X 先序于 B ,则 B 观测到以下之一:

  • 在单独全序中先出现于 X 的上个 M 的 memory_order_seq_cst 修改
  • 在单独全序中后出现于它的某些 M 的无关联修改

设有一对 M 上的原子操作,称之为 A 和 B ,这里 A 写入、 B 读取 M 的值,若存在二个 memory_order_seq_cststd::atomic_thread_fence X 和 Y ,且若 A 先序于 X , Y 先序于 B ,且 X 在单独全序中先出现于 Y ,则 B 观测到二者之一:

  • A 的效应
  • 某些在 M 的修改顺序中后出现于 A 的无关联修改

设有一对 M 上的原子操作,称之为 A 和 B ,若符合下列条件之一,则 M 的修改顺序中 B 先发生于 A

  • 存在一个 memory_order_seq_cststd::atomic_thread_fence X ,它满足 A 先序于 X ,且 X 在单独全序中先出现于 B
  • 或者,存在一个 memory_order_seq_cststd::atomic_thread_fence Y ,它满足 Y 先序于 B ,且 A 在单独全序中先出现于 Y
  • 或者,存在 memory_order_seq_cststd::atomic_thread_fence X 和 Y ,它们满足 A 先序于 X , Y 先序于 B ,且 X 在单独全序中先出现于 Y

注意这表明:

1) 只要不带 memory_order_seq_cst 标签的原子操作进入局面,则立即丧失序列一致性
2) 序列一致栅栏仅为栅栏自身建立全序,而不为通常情况下的原子操作建立(先序于不是跨线程关系,不同于先发生于
(C++20 前)

正式而言,

某原子对象 M 上的原子操作连贯先序于 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 操作,则 S 中 A 前于 B
b) 若 A 是 memory_order_seq_cst 操作,而 B 先发生于 memory_order_seq_cst 栅栏 Y ,则 S 中 A 前于 Y
c)memory_order_seq_cst 栅栏 X 先发生于 A ,而 B 为 memory_order_seq_cst 操作,则 S 中 X 前于 B
d)memory_order_seq_cst 栅栏 X 先发生于 A ,而 B 先发生于 memory_order_seq_cst 栅栏 Y ,则 S 中 X 前于 Y

正式定义确保:

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

单独全序可能与先发生于不一致。这允许 memory_order_acquirememory_order_release 在某些 CPU 上的更高效实现。当 memory_order_acquirememory_order_releasememory_order_seq_cst 混合时,这能产生惊异的结果。

例如,对于初值为零的 xy

// 线程 1 :
x.store(1, std::memory_order_seq_cst); // A
y.store(1, std::memory_order_release); // B
// 线程 2 :
r1 = y.fetch_add(1, std::memory_order_seq_cst); // C
r2 = y.load(std::memory_order_relaxed); // D
// 线程 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 ,但 memory_order_seq_cst 的单独全序 C-E-F-A 中 C 前于 A (见 Lahav 等)。

注意:

1) 一旦不带 memory_order_seq_cst 标签的原子操作进入局面,程序的序列一致保证就会立即丧失
2) 多数情况下, memory_order_seq_cst 原子操作相对于同一线程所进行的其他原子操作可重排
(C++20 起)

若在多生产者-多消费者的情形中,且所有消费者都必须以相同顺序观察到所有生产者的动作出现,则可能必须有序列顺序。

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

此示例演示序列一直顺序为必要的场合。任何其他顺序都可能触发assert,因为可能令线程cd观测到原子对象xy以相反顺序更改。

#include <thread>
#include <atomic>
#include <cassert>
 
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);  // 决不发生
}

volatile 的关系

在执行线程中,不能将通过 volatile 泛左值的访问(读和写)重排到同线程内先序于后序于它的可观测副效应(包含其他 volatile 访问)后,但不保证另一线程观察到此顺序,因为 volatile 访问不建立线程间同步。

另外, volatile 访问不是原子的(共时的读和写是数据竞争),且不排序内存(非 volatile 内存访问可以自由地重排到 volatile 访问前后)。

一个值得注意的例外是 Visual Studio ,其中默认设置下,每个 volatile 写拥有释放语义,而每个 volatile 读拥有获得语义( MSDN ),故而可将 volatile 对象用于线程间同步。标准的 volatile 语义不可应用于多线程编程,尽管它们在应用到 sig_atomic_t 对象时,足以与例如运行于同一线程的 std::signal 处理函数交流。

参阅

外部链接