99网
您的当前位置:首页内存屏障 Memory Barriers

内存屏障 Memory Barriers

来源:99网

内存屏障

在上一篇文章中我们提到了编译时的内存序重排导致的问题以及解决方法,即添加编译器屏障或处理器屏障指令。这篇文章将探讨内存屏障的语义。

内存屏障的类型 Types of Memory Barrier

内存屏障的作用是避免不期望的内存操作乱序,使得代码编译时和运行时按照我们期望的方式进行。内存屏障可以分为四种类型,实际的 CPU 屏障指令表现为这四种类型的一种或多种的组合,或者带上其他的一些副作用。

Load Load

Load Load 类型的屏障阻止对屏障前后的 Load 操作进行重排。

if (IsPublished)                   // Load and check shared flag
{
    LOADLOAD_FENCE();              // Prevent reordering of loads
    return Value;                  // Load published value
}

这里保证了必须先读取 IsPublished,条件满足后再读取 Value,可以避免获取到过期的 Value 值。

Store Store

Store Store 类型的屏障阻止对屏障前后的 Store 操作进行重排。

Value = x;                         // Publish some data
STORESTORE_FENCE();
IsPublished = 1;                   // Set shared flag to indicate availability of data

这里保证了 Value 值被更新后才会对 IsPublished 赋值,提示其他人 Value 可读。

Load Store

Load Store 类型的屏障阻止对屏障前的 Load 及其后的 Store 操作乱序。屏障前的 Load 操作将对其他处理器可见。

Store Load

Store Load 类型的屏障阻止对屏障前的 Store 及其后的 Load 操作乱序。屏障前的 Store 操作将对其他处理器可见。这个屏障特性的开销往往比其他的都要大。

获取与释放语义 Acquire and Release Semantics

这里尽量贴近 C++11 原子操作库的语义进行阐述。

  • 获取语义是只能应用于读取共享内存单元操作的属性,不管这个读取操作是读-修改-写,还是单纯的读取。Acquire 语义阻止读获取与读获取之后的所有内存读写操作乱序。

  • 释放语义只是能应用于写共享内存单元操作的属性,不管这个写操作是读-修改-写,还是单纯的写入。Release 语义阻止写释放与写释放之前的所有内存读写操作乱序。

通过上面的描述,我们可以用前面的四种屏障类型来构造出想要的语义。

  • Acquire 语义可以用 Load Load & Load Store 来实现。语义要求读获取与之后的读写操作不能乱序,上述的屏障就能实现这个功能。
  • Release 语义可以用 Load Store & Store Store 来实现。语义要求写释放与之前的读写操作不能乱序,上述的屏障就能实现这个功能。

细心的读者也许已经有疑问了,上面两个语义都没用到 Store Load 类型的屏障。举例来说,PowerPC 中的 lwsync 指令(lightweight sync)表现为 #LoadLoad, #LoadStore and #StoreStore 类型的屏障。这比 sync 指令要更轻便,sync 指令还包含了 #StoreLoad 类型的屏障。

read-Acquire  <-- (Load Load & Load Store)
----------------------
all memory operations
stay below the line
...
...
...
all memory operations
stay above the line
----------------------
write-Release  <-- (Load Store & Store Store)

使用显式的平台相关的屏障指令 With Explicit Platform-Specific Fence Instructions

我们这里以一个简单的例子来说明。假设我们在为 PowerPC 编程。__lwsync() 是编译器内嵌的函数。前面已经描述了该指令可带有多种屏障类型效果。我们可以在下面的代码中构造出获取和释放语义。在线程1中,对 Ready 的赋值需要一个写释放语义,线程2中从 Ready 读取则需要一个读获取语义。

// Shared global varible
int A = 0;
int Ready = 0;

// Thread 1
A = 42;
__lwsync();     <-- 这里实际上只需要 Store Store 屏障
                <-- Keep all memory operations above this line
Ready = 1;      <-- This becomes the write-release

// Thread 2
int r1 = Ready; <-- This becomes the read-acquire
                <-- Keep all memory operations below this line
__lwsync();     <-- 这里实际上只需要 Load Load 屏障
int r2 = A;

通过上面的实现,我们可以保证,只要我在线程2中读取到 r1 == 1,则必定有 r2 == 42。读获取与写释放语义始终作用于同一个变量,可以看到从始至终我们都在对 Ready 的读写进行保护。当然读写需要是原子的,由于 Ready 为一个对齐的 int 类型,该类型在 PowerPC 上读写已经是原子的了。

使用可移植的 C++11 屏障 With Fences in Portable C++11

为了可移植性,我们可以用 C++11 的原子库函数。atomic_thread_fence() 这个接口只有一个参数,用于指定屏障的类型。最常用的类型是 memory_order_acquirememory_order_release,可以用这个函数替代 __lwsync()。在 PowerPC 上,对 Ready 的读写是原子的,为了更好的可移植性,我们这里重新声明类型为 atomic。似乎现代 CPU 对 int 类型的操作都是原子的。

// Shared global varible
int A = 0;
atomic<int> Ready(0);

// Thread 1
A = 42;
atomic_thread_fence(memory_order_release);
                <-- Keep all memory operations above this line
Ready.store(1, memory_order_relaxed);

// Thread 2
int r1 = Ready.load(memory_order_relaxed);
                <-- Keep all memory operations below this line
atomic_thread_fence(memory_order_acquire);
int r2 = A;

memory_order_relaxed 的含义是保证操作是原子的,但是不添加任何未显式指定的序关系约束或者内存屏障。

可以想见,在不同的平台上 atomic_thread_fence() 将有不同的实现,在 PowerPC 上可能还是 lwsync,在 ARM 上可能是 dmb。而在 x86/上,实现可能只是一个编译器屏障,因为通常它的每个 Load 操作已经带有 acquire 语义,每个 Store 操作已经带了 release 语义,这也是 x86/ 通常被称为强类型序机器的原因。

当然也可以直接在 Ready 的操作里直接带上相关的语义而不显式调用屏障函数。这个操作相比于显式调用屏障函数在某些平台上会稍微弱一些。当然直接在操作中带上相关语义是更受欢迎的一种方式,也许是写得方便 😃?

// Thread 1
A = 42;
Ready.store(1, memory_order_release);

// Thread 2
int r1 = Ready.load(memory_order_acquire);
int r2 = A;

锁机制中的获取与释放 Acquire and Release While Locking

前文的描述中关于获取与释放的语义中似乎没有 Load Store 类型屏障的场景。那这个屏障什么时候会派上用场呢?一个场景是实现互斥锁 mutex lock 的时候。事实上,这就是获取与释放语义名称的来源:获取锁意味着获取语义,而释放锁意味着释放语义。

pthread_mutex_lock(&mutex);
-----------------------------
all memory operations stay
between the lines
-----------------------------
pthread_mutex_unlock(&mutex);

获取与释放语义确保在持锁期间做的修改都将被下一个持锁的线程感知到。

因篇幅问题不能全部显示,请点此查看更多更全内容