无锁编程教程:Acquire和Release语义

  • 这是无锁编程教程的第四篇文章。
  • 本文详细讲解了内存顺序中Acquire和Release的含义,包括了如何使用甚至屏障命令以及使用C++11的原子数据类型库实现屏障。
  • 如果你发现本文使用了一些没有解释的术语,那么很有可能我已经在本系列之前的几篇文章中有所涉及。
  • 原文地址:http://preshing.com/20120913/acquire-and-release-semantics/

在无锁编程中,线程有两种方法操纵他们的共享内存:他们既可以通过竞争的方式抢夺某一种资源,也可以借助共享内存传递信息来实现相互之间的合作。Acquire和Release对于后者来说是至关重要的:他们可以让信息的传递变得可靠,实际上,缺少或者不正确的Acquire/Release使用是无锁编程中的一个经典错误!

在这篇文章中,我将向大家介绍若干种通过C++实现Acquire/Release的方式。作为引入,我将以最快的速度过一遍C++11的原子类型库,因此,你并不一定需要立即读懂。同时,这篇文章仍然默认我们的编程都不满足顺序一致性。我们将直面存在内存重排问题的多核处理器环境。

不幸的是,倘若你尝试通过搜索网上来学习他们,你会发现Acquire和Release术语的定义过于混乱,根本不知道在说些什么。不过,Bruce Dawson在他的论文中给我们了一系列非常靠谱的定义,而我在这里,也将向大家介绍我自己归纳出的一些定义,尽可能的与现存的C++11原子数据类型相吻合。

Acquire semantics is a property that can only apply to operations that read from shared memory, whether they are read-modify-write operations or plain loads. The operation is then considered a read-acquire. Acquire semantics prevent memory reordering of the read-acquire with any read or write operation that follows it in program order.

Acquire语义修饰内存读操作(包括内存读取或者读-修改-写操作),倘若一个操作被该修饰符修饰(read-acquire),那么他就能够防止其后面的所有内存读/写操作重排到他的前面。

Release semantics is a property that can only apply to operations that write to shared memory, whether they are read-modify-write operations or plain stores. The operation is then considered a write-release. Release semantics prevent memory reordering of the write-release with any read or write operation that precedes it in program order.

Release语义修饰内存写操作(包括内存修改或者读-修改-写操作),倘若一个操作被该修饰符修饰(write-release),那么他就能够防止其前面的所有内存读/写操作重排到他的后面。

一旦你仔细理解了上述定义,你就会发现acquire和release操作可以通过上一篇文章中说的内存屏障实现——将内存屏障通过某种方式放在read-acquire操作后,或者write-acquire操作前面,这样,内存屏障就能起到acquire/release相同的作用。不过,需要注意的是,从技术上来说还会复杂一些,不过并不影响我们理解。

一个很好的地方是,acquire和release都不需要使用#StoreLoad屏障,也就是说,他们总不会太低效。举一个例子,在PowerPC中,lwsync(全称”lightweight sync”,即轻量级同步)指令表现为#LoadLoad,#LoadStore和#StoreStore屏障的组合,在这种情况下仍然比sync要快很多,而sync就是一个#StoreLoad屏障。

直接使用平台的屏障指令

实现我们希望的内存屏障的一种方法是直接使用平台指定的屏障指令,让我们从一个简单的例子出发,假设我们在PowerPC上面编程,而 __lwsync() 是一个编译器的内部用于生成 lwsync 的函数。由于 lwsync 同时引入了多种屏障,因此,不论我们希望使用release语义还是acquire语义,我们都可以用同样的方式建立屏障——在线程1中,将对于 Ready 变量的储存转化为write-release,并在线程2中,将对于 Ready 变量的读取转化为read-acquire.

如果我们让两个线程执行结果为 r1==1 ,那么我们可以确信在第一个线程中,A的赋值操作成功的传递到了线程中,进而保证了 r2 == 42 。在我之前的文章中,我已经详细讲解了#LoadLoad和#StoreStore的原理,因此就不在这里赘述了。

用正规的术语表示,我们可以说:变量 Ready 与数据的读取是同步(synchronized-with)的。而对于同步的理解,可以参见这里。至少我们现在可以肯定这样的策略是可行的,但是他们必须作用于同一个变量上,比如这里的 Ready ,而且读取和储存必须是原子的。由于在这里,Ready是一个int类型,因此它的读写操作在PowerPC上面本身就是原子的。

在C++11中使用屏障指令

在之前的样例中,为了实现屏障代码,我们需要仔细了解编译器以及处理器的版本。并在此基础上设计代码。而假设我们希望写一个支持多处理器的代码,我们可以使用C++11重写我们的代码。在C++11中和原子操作有关的所有的数据类型都存在于std命名空间中,我们在之后的代码中,默认包含 using namespace std; 。

在C++原子库中定义了一个可移植的函数 atomic_thread_fence() ,这个函数接受一个参数来决定屏障的种类。当然,比如 memory_order_acquire 和 memory_order_acquire 就是一种选择,我们可以把这个函数放在之前我们放 __lwsync() 的地方。

不过,还有一点点小小的缺陷,那就是在PowerPC,我们可以保证 Ready 是一个原子变量,但是我们不能够假设这适合与所有的平台,因此我们应该将 int 改为 atomic<int> 。唔,这确实是一个挺愚蠢的改变,因为现在所有CPU对于int的读写都是原子的。不过我将在这里详细叙述这个问题,但是至少现在为止,我们使用这个愚蠢的修改来保证我们理论上100%的正确率吧。

在代码中,参数 memory_order_relaxed 意思是“保证这个操作是原子的,但是我并不指定一个特定的内存顺序限制。”

重新申明一些,所有之前的 atomic_thread_fence() 调用都能够(并很有可能确实是)用PowerPC上的  lwsync 替代的,他们都可以在ARM中生成 dmb 指令,在PowerPC上面, atomic_thread_fence() 也至少不比 lwsync 糟糕。在x86/64上面, atomic_thread_fence() 都被解析成了编译器屏障,因为所有的x86/64的读写操作都自动蕴含了acquire/release语义。这也是为什么x86/64是强顺序(strongly ordered)的。

在C++11中不使用屏障指令

在C++11中,我们实际上可以不显示指定内存屏障指令来实现屏障,我们只需要对于Ready的指令显示指定内存顺序要求就好了。

考虑将所有屏障指令包含在Ready的修改指令自身(注意,这种形式不完全和在外面定义屏障指令相同,前者是不那么严格的)。编译器会生成必要的指令来保证屏障的效果,比如在Itanium里面,所有操作可以被表示为单个指令 ld.acq 和 st.rel 。正如之前所说的,这样的操作保证了 r1 == 1 能够决定 r2 == 42 一定成立,也就是保证了同步(synchronizes-with)关系。

这也是C++11推荐的,表达acquire/release语义的方式,事实上, atomic_thread_fence() 的加入甚至比标准的指定还要晚。

Acquire 和 Release 和锁

正如你所看到的,上述所有的例子都没有使用acquire和release语义引出的#LoadStore屏障,只有#LoadLoad和#StoreStore是真正有用的,这是因为在这个文章中,我选择最简单的例子让大家理解API和语法。

一个#LoadStore比较重要的例子是我们使用acquire和release语义实现一个锁。实际上,这也是acquire和release两个词的来历——获取(acquire)一个锁,和释放(releaseing)一个锁。所有的内存操作内存操作都被这两个命令关在了一个小区域内,进保证锁的临界区的有效性。

在这里,acquire和release语义保证了在持有锁的时候的修改都在锁里面,所有锁的的实现(包括你自己实现的锁),都需要保证这些要求,而这些都是为了能够有效地在两个线程中传递信息,即使是在多核、多处理器环境中。

在之后的一系列文章中,我会展示一个用C++11实现的演示。他可以在真实的硬件上运行,而其中,acquire和release保证了其运行的稳定性。

原创文章地址:【无锁编程教程:Acquire和Release语义】,转载时请注明出处mhy12345.xyz

《无锁编程教程:Acquire和Release语义》有2个想法

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.