C语言原子操作的应用(内存次序,内存屏障)

内存次序

为优化程序代码,编译器和处理器可以自由地对任何无相互依赖关系的命令进行重新排列。例如,两个分配语句 a=0;B=1;,它们可以以任一顺序执行。然而,在多线程环境下,由于不同线程内存操作之间的依赖性对于编译器或处理器通常是不可见的,所以对编译器或处理器执行命令重新排序可能会引发错误。

使用原子对象可以默认地防止此类重新排序。但是,防止优化意味着可能会牺牲速度。有经验的程序员可以在较低的内存次序请求下,通过明确地使用原子操作提高性能。

对于每个执行原子操作的函数(例如 atomic_store()),都有另一个版本,这些函数的名称以 _explicit 结尾,如 atomic_store_explicit(),它们增加了一个类型为 memory_order 的参数。

memory_order 类型是一个枚举,它定义了以下常量,以指定特定的内存次序请求:

memory_order_relaxed

调用者指定无任何内存次序请求,从而使编译器可以自由地改变操作的顺序。

memory_order_release

指定在当前线程 T1 中对一个原子对象A进行写访问时执行释放操作(release operation)。释放操作的作用是:当另一个线程 T2 对 A 执行捕获操作时(读访问),所有 T1 曾对 A 执行的操作在 T2 捕获 A 以后,对 T2 都是可见的。

memory_order_acquire

指定对一个原子对象进行读访问时执行捕获操作(acquire operation)。它确保在该函数调用前,后续的内存访问操作不发生重新排列。

memory_order_consume

一个消耗操作(consume operation)的限制小于一个捕获操作:它仅当后续内存访问操作直接依赖读取原子变量时,防止重新排序。

memory_order_acq_rel

同时具有捕获和释放操作。

memory_order_seq_cst

顺序一致性(sequential consistency)请求包括对 memory_order_acq_rel 的捕获和释放操作。此外,它还指定了所有操作按一个次序严格执行,该次序为所包含原子对象的修改次序。顺序一致性是默认的内存顺序请求,如果没有显式指定更低的请求,这种请求会应用到所有原子操作。

如果将 counter 声明为原子对象,自增和自减 counter 都是独立于其他操作的,因此不必指定内存访问次序。换句话说,在下述语句位置:
++counter;            // 隐含memory_order_seq_cst

下面语句充分且允许编译器执行更多的优化:
atomic_fetch_add_explicit( &counter, 1, memory_order_relaxed );

释放和捕获操作是在命令间建立 happens-before 关系的有效途径。换句话说,如下例所示,_explicit 函数确保一个线程完成操作 A 后才能执行操作 B:
struct Data *dp = NULL, data;
atomic_intptr_t aptr = ATOMIC_VAR_INIT(0);

// 线程1
   data = ...;                                          // 操作A
   atomic_store_explicit( &aptr, (intptr_t)&data,
                          memory_order_release );

// 线程2
   dp = (struct Data*)atomic_load_explicit( &aptr,
                                            memory_order_acquire );
   if( dp != NULL)
      // 处理*dp所引用的数据
                                                            // 操作B
   else
      // *dp所引用的数据还不可获得

对于一个使用互斥同步的程序,当互斥锁定时,隐含了一个捕获操作,当互斥解锁时,隐含了一个释放操作。这意味着:如果一个线程 T1 使用互斥来保护一个操作 A,而另一个线程 T2 使用相同的互斥来保护一个操作 B,假如 T1 先锁定互斥,那么操作 A 将先完成执行,然后才执行操作 B。相反,假如 T2 先锁定互斥,那么当 T1 执行操作 A 时,通过操作 B 所执行的所有修改,对于线程 T1 是可见的。

栅栏(内存屏障)

对于一个原子操作的内存次序请求,也可以通过一个原子操作单独指定。这种技术被称为建立一个栅栏(fence)内存屏障(memory barrier)。要设置一个栅栏,C11 提供了以下函数:
void atomic_thread_fence(memory_order order);

若参数值为 memory_order_release,函数 atomic_thread_fence()建立一个释放栅栏(releas fence)。在这种情况下,原子写操作必须在释放栅栏之后发生

若参数值为 memory_order_acquire 或 memory_order_consume,函数 atomic_thread_fence()建立一个捕获栅栏(acquire fence)。在这种情况下,原子读操作必须在捕获栅栏之前发生

栅栏允许更大程度的内存顺序优化。
// 线程2
   dp = (struct Data*)atomic_load_explicit( &aptr, memory_order_relaxed );
   if( dp != NULL)
   {
      atomic_thread_fence(memory_order_acquire);
      // 操作B:处理*dp所引用的数据
   }
   else
      // *dp所引用的数据还不可获得