非常教程

C参考手册

原子操作 | Atomic operations

memory_order

在头文件<stdatomic.h>中定义

enum memory_order {memory_order_relaxed,memory_order_consume,memory_order_acquire,memory_order_release,memory_order_acq_rel,memory_order_seq_cst};

(自C11以来)

memory_order指定如何在原子操作周围定期进行非原子内存访问。在多核系统上不存在任何约束时,当多个线程同时读取和写入多个变量时,一个线程可以按照与另一个线程写入它们的顺序不同的顺序观察值的变化。事实上,在多个读者线程中,更改的顺序甚至可能不同。由于内存模型允许的编译器转换,即使在单处理器系统上也会出现类似的效果。

语言和库中的所有原子操作的默认行为提供了顺序一致的排序(参见下面的讨论)。该默认值可能会损害性能,但可以给库的原子操作提供额外的memory_order参数,以指定编译器和处理器必须为该操作强制执行的确切约束,而不仅限于原子性。

常量

| 在头文件<stdatomic.h> |中定义

|:----|

| Value | Explanation |

| memory_order_relaxed | 轻松的操作:对其他读取或写入没有同步或排序约束,只保证此操作的原子性(请参阅下面的轻松排序)。

| memory_order_consume | 使用此内存顺序的加载操作会在受影响的内存位置执行消耗操作:根据当前加载的值,当前线程中的读取或写入操作在此加载之前可以重新排序。在释放相同原子变量的其他线程中写入数据相关变量在当前线程中可见。在大多数平台上,这仅影响编译器优化(请参阅下面的Release-Consume命令)|

| memory_order_acquire | 使用此内存顺序的加载操作对受影响的内存位置执行获取操作:在此加载之前,当前线程中的读取或写入操作不能重新排序。在当前线程中可以看到释放相同原子变量的其他线程中的所有写入操作(请参阅下面的Release-Acquire命令)|

| memory_order_release | 与此存储器顺序的存储操作执行释放操作:没有读取或在当前线程可以在此存储之后被重新排序写入。在当前线程所有写是在获得相同的原子变量其他线程可见(见发布 - 采集以下的订购)和写携带的依赖到原子变量中消耗相同的原子其他线程变得可见(见释放,消费订购如下)。|

| memory_order_acq_rel | 使用该存储器命令的读取 - 修改 - 写入操作既是获取操作又是释放操作。在当前线程中没有内存读取或写入可以在该存储之前或之后重新排序。在其他线程中释放相同原子变量的所有写操作在修改之前都是可见的,并且修改在获取相同原子变量的其他线程中可见。|

| memory_order_seq_cst | 此存储器订单的任何操作都是采集操作和释放操作,并且存在单个总订单,其中所有线程都以相同的顺序观察所有修改(请参见下面的按顺序一致的排序)。

轻松订购

标记的原子操作memory_order_relaxed不是同步操作; 它们不会在并发内存访问中强加一个顺序。他们只保证原子性和修改顺序的一致性。

例如,对于xy最初为零,

// Thread 1:

r1 = atomic_load_explicit(y, memory_order_relaxed); // A

atomic_store_explicit(x, r1, memory_order_relaxed); // B

// Thread 2:

r2 = atomic_load_explicit(x, memory_order_relaxed); // C

atomic_store_explicit(y, 42, memory_order_relaxed); // D.

被允许产生r1 == r2 == 42,因为尽管A被测序-之前线程1中B和C 之前测序线程2内d,没有什么阻止d从在y的修改次序出现A之前和B从在修改次序出现在C之前的x。线程1中的负载A可以看到D对y的副作用,而线程2中的负载C可以看到B对x的副作用。

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

发布 - 消费订购

如果线程A中的原子存储被标记memory_order_release并且来自同一变量的线程B中的原子加载被标记memory_order_consume,则所有存储器从原子存储之前写入(非原子和放宽原子)线程A 在线程B中的那些操作内成为可见的副作用,负载操作携带依赖性,即一旦原子加载完成,线程B中的那些使用从加载获得的值的运算符和函数被保证为看看写到内存的线程是什么。

同步仅在释放使用相同原子变量的线程之间建立。其他线程可以看到不同的存储器访问顺序,而不是任何一个或两个同步线程。

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

这种排序的典型用例涉及读取访问很少写入的并发数据结构(路由表,配置,安全策略,防火墙规则等)以及使用指针中介发布的发布者订阅者情况,也就是说,当生产者发布指针时消费者可以访问这些信息:不需要将生产者写入内存的所有内容都写入消费者可以看到的内存中(这可能是对弱排序架构的昂贵操作)。这种情况的一个例子是rcu_dereference。

请注意,目前(2015年2月)没有已知的生产编译器跟踪依赖链:消耗操作被解除以获取操作。

Release sequence

如果一些原子被存储释放并且其他几个线程对该原子执行读 - 修改 - 写操作,则形成“release sequence”:执行读取 - 修改的所有线程 - 写入相同的原子与第一线程同步并且即使他们没有memory_order_release语义,也是如此。这使单个生产者 - 多个消费者情况成为可能,而不会在各个消费者线程之间施加不必要的同步。

发布 - 获取订购

如果线程A中的原子存储被标记memory_order_release并且来自同一变量的线程B中的原子加载被标记memory_order_acquire,则所有存储从线程A的角度在原子存储之前写入(非原子和放宽原子)在线程B中变成可见的副作用,也就是说,一旦完成了原子加载,线程B就会保证看到线程A写入内存的所有内容。

同步仅在释放获取相同原子变量的线程之间建立。其他线程可以看到不同的存储器访问顺序,而不是任何一个或两个同步线程。

在高度有序的系统(x86,SPARC TSO,IBM大型机)上,大多数操作的发布采集排序是自动的。对于此同步模式,不会发出额外的CPU指令,只会影响某些编译器优化(例如,禁止编译器将原子存储释放移出原子存储释放或在原子载入获取之前执行非原子加载)。在弱有序的系统(ARM,Itanium,PowerPC)上,必须使用特殊的CPU负载或内存防护指令。

相互排斥锁(如互斥锁或原子螺旋锁)是发布 - 获取同步的一个示例:当锁由线程A释放并由线程B获取时,上下文中关键部分(释放之前)发生的所有事件线程A必须对正在执行相同关键部分的线程B(获取之后)可见。

顺序一致的排序

原子操作memory_order_seq_cst不仅以与释放/获取顺序相同的方式标记内存(发生的所有事情 -在一个线程中的存储变成执行加载的线程中的可见副作用之前),而且还建立了所有的修改顺序原子操作如此标记。

从形式上看,

memory_order_seq_cst从原子变量M加载的每个操作B都遵循以下之一:

  • 最后一个操作A的结果是修改后的M,它出现在单个总订单中的B之前
  • 或者,如果有这样一个A,那么B可以观察到M上的一些修改的结果,memory_order_seq_cst而不是 A 之前发生并且不发生
  • 或者,如果没有这样的A,则B可以观察到M的一些不相关的修改的结果 memory_order_seq_cst

如果 B 之前有一个排序memory_order_seq_cst atomic_thread_fence操作X ,则B会观察以下之一:

  • memory_order_seq_cst在单个总订单中出现在X之前的M 的最后修改
  • M的修改顺序中稍后出现的一些与M无关的修改

对于M上的一对称为A和B的原子操作,其中A写入并且B读取M的值,如果有两个memory_order_seq_cst atomic_thread_fences X和Y,并且如果A被排序 - 在 X 之前,Y被排序 - 在 B 之前,并且X出现在单一全部订单中Y之前,则B观察到:

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

对于称为A和B的M的一对原子修饰,B在M的修饰顺序为A之后发生。

  • 有一个memory_order_seq_cst atomic_thread_fenceX使得A被排序 - 在 X和X出现在单个全部顺序中的B之前
  • 或者,有一个memory_order_seq_cst atomic_thread_fenceY使得Y 排序 - 在 B和A之前出现在单个总排序中的Y 之前
  • 或者,存在memory_order_seq_cst atomic_thread_fenceS和X,使得A被排序 - 在 X 之前,Y B 之前排序,并且X在单个总排序中出现在Y之前。

请注意,这意味着:

1)一旦没有标记的原子操作memory_order_seq_cst进入图片,就会失去顺序一致性

2)顺序一致的栅栏只为栅栏本身建立总排序,而不是针对一般情况下的原子操作(顺序 - 之前不是跨线程关系,不像以前发生的那样)

对于多个生产者 - 多个消费者情况,序列排序可能是必要的,其中所有消费者都必须遵守以相同顺序出现的所有生产者的行为。

全部顺序排序需要在所有多核系统上提供完整的内存围栏CPU指令。这可能会成为性能瓶颈,因为它会迫使受影响的内存访问传播到每个内核。

与volatile关系

在一个执行线程中,通过易变的左值访问(读写)不能重新排序超过由相同线程中的序列点分隔的可观察副作用(包括其他易失性访问),但不保证此顺序被观察到由另一个线程,因为易失性访问不建立线程间同步。

此外,易失性访问不是原子性的(并发读写是数据竞争),也不会对内存进行排序(非易失性内存访问可以在易失性访问周围自由重新排序)。

一个值得注意的例外是Visual Studio,其中默认设置下,每个易失性写入都具有释放语义,每个易失性读取都获取语义(MSDN,因此挥发性可用于线程间同步,标准volatile语义不适用于多线程编程,尽管它们足以用于与signal应用于sig_atomic_t变量时在同一线程中运行的处理程序进行通信。

示例

参考

  • C11标准(ISO/IEC 9899:2011):
    • 7.17.1/4 memory_order(p: 273)
    • 7.17.3订单和一致性(p: 275-277)

另请参阅

| 用于记忆顺序的C ++文档|

|:----|

外部链接

  • MOESI协议
  • x86-TSO:x86多处理器的严格且可用的程序员模型 P. Sewell et。2010年
  • ARM和POWER宽松内存模型教程简介 P. Sewell等,2012
  • MESIF:点对点互连的两跳高速缓存一致性协议 JR Goodman,HHJ Hum,2009
C

C 语言是一门通用计算机编程语言,应用广泛。C 语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。