本文档翻译自Dr.Dobb's:Lock-Free Data Structures或者通过这个来看。
无锁数据结构当执行多线程程序的时候保证至少一个线程的执行进程,因此可以帮助你避免死锁的出现。
Andrei Alexandrescu是《现代C++设计》的作者,是Washington大学的计算机学院的毕业生。
在跳过一期"Generic<Programming>"之后,直到这篇文章作为候选主题出来之前,一直有一种好东西太多而不知道怎么选择的烦恼(an embarrassment of riches)。其中一个候选主题是对于构造函数的讨论,尤其是转发构造函数,异常处理和两阶段对象构造。其中另一个候选主题是给未完成类型创建容器,这种容器在一套有奇技淫巧的帮助下是可以实现的,但是标准容器并不保证。
尽管这两个候选主题都非常的吸引人,但当跟无锁数据结果这个主题比起来他们不会有希望的(stand a chance),无锁数据结构目前在多线程编程社区中正在流行(all the rage)。在今年的Programming Language Design and Implementation 会议上,Michael Maged展示了世界首个无锁内存分配器,这个无锁内存分配器在很多测试中超过了更复杂和精心设计基于锁的分配器。
下面是最近最新的一些无锁数据结构以及算法。
无锁,你是什么意思?
这个是前一阵子我提出的问题。作为一个主流的善意的多线程程序员,我对基于锁的多线程算法非常熟悉。在典型的基于锁的编程中,无论何时你要共享数据,你必须串行地访问它。改变数据的操作必须以原子形式出现,这样才没有其他线程介入来损坏数据的不变性。即使一个简单操作,例如++count_(count_是一个整型类型)必须被锁住。自增其实是一个三步操作(读取、修改、写回)而非原子的。
简言之,在基于锁的多线程编程中,你需要保证在共享数据上的任何可能出现竞态的操作都通过对互斥锁加锁和解锁来实现原子性。好的一面就是,只要互斥锁被锁住了,你可以执行几乎任何操作,其他线程不会破坏你的共享状态。
这种在互斥锁加锁之后,你可以随心所欲操作的状态也是有问题的。例如,你可以读取键盘或者做慢速IO操作,这意味着你延迟了等待这个互斥锁的任何其他线程。更糟糕的是,你可能想要访问其他的共享数据,并尝试去锁住对应的互斥锁。如果刚好一个其他线程已经锁住了最后一个互斥锁并想要访问你线程已经锁住的那把互斥锁,两个线程会比你意识到死锁更快的阻塞。
进入无锁编程。在无锁编程中,你不能原子地做所有的事情。只有很少的一些事情可以原子地做。这个限制条件让无锁编程的方式变难。实际上,世界上差不多只有6个无锁编程专家,我也不在其列。幸运的是,这个文章向你提供基本的工具,参考文献,以及热情来帮助你成为其中的一个。这样一个稀缺的框架的回报是,你可以更好的保证线程间的互操作和线程的进展。那么,在无锁编程中你可以原子实现的“很少的一些事情”是什么呢?如果真的有的话,那么什么是可以实现任何无锁算法的最小的原子原语(atomic primitives)集合?
如果你认为给这个问题的回答者颁奖是一个基本问题的话,那么其他人也如此。2003年,Maurice Herlihy因其1991年开创性的论文“Wait-Free Synchronization”而被授予Edsger W. Dijkstra分布式计算奖。在他的论文中,Herilhy证明了对于构建无锁数据结构哪些原语是好的,哪些是坏的。这使得一些看似很热门的硬件架构瞬间过时,同时阐明在今后的硬件中应该要实现哪些同步原语。例如,Herlihy的论文给出了否定的结果,表明原子操作如测试和设置,交换,提取和添加,甚至原子队列(!)都不足以正确同步两个以上的线程。 (这是令人惊讶的,因为具有原子推送和弹出操作的队列提供了相当强大的抽象。)从好的方面来看,Herlihy也给出了普遍性结果,证明一些简单的结构足以实现任何数量的线程的任何无锁算法。
最简单的也是最流行的普遍原语,也是我一直在使用的,就是CAS(compare-and-swap)操作:
template<class T>
bool CAS(T* addr, T expected, T value)
{
if (*addr == expected)
{
*addr = value;
return true;
}
return false;
}
CAS将内存地址的内容与预期值进行比较,如果比较成功,则用新值替换内容。 整个过程都是原子的。 许多现代处理器为不同的位长度实现CAS或等效原语(这个是我们将其作为模板的原因,假设实现使用元编程来限制可能的Ts)。 根据经验,CAS可以原子地比较和交换的位越多,用它实现无锁数据结构就越容易。 今天的大多数32位处理器都采用64位CAS; 例如,英特尔的汇编程序将其称为CMPXCHG8(您必须喜欢那些汇编程序助记符)。
警告的话
通常一个C++文章是带有C++代码片段和实例的。理想情况下,代码是标准C++的,并且后面的"泛型编程"会努力去实现这个理想。
然而当写多线程代码的时候,给标准C++的实例根本不可能。标准C++中没有线程,总不能用不存在的东西去编码。因此,本文采用伪代码进行描述。就拿内存屏障为例,这里描述的算法要么用汇编语言翻译,或者至少要把C++代码和一些能强制内存读写顺序且依赖于处理器的“魔法”的所谓的内存屏障结合起来运用。除了无锁数据结构之外,我不想通过内存屏障来扩展讨论。出于此目的我假设编译器和硬件不会引入那些时髦的特性(比如消除冗余读取和在单线程假设下进行有效优化)。技术上讲,这种叫做“顺序一致模型”,也就是读和写执行的和看到的都是严格按照代码中看到的那样的顺序。
无等待和无阻赛 V.S. 有锁
一个无等待的过程,不论其他进程的相对速度都可以在有限数量的步数之后完成。
一个无锁的进程保证至少有一个线程执行这个过程。也就意味着部分线程能被任意延后,但是可以保证至少有一个线程在每一步都有进展。因此整个系统作为一个整体总是有进展的,尽管其中一些线程可能进展比其他线程慢。