无锁数据结构(一)

来源:互联网 发布:固定收益部 知乎 编辑:程序博客网 时间:2024/06/05 14:35

无锁数据结构(一)

Andrei Alexandrescu

December 16, 2007

译者:张桂权

12/25/2007

初稿阶段,没有得到许可不得引用,否则后果自负

泛型编程(Generic<Programming>)被删除之后(我知道,认为母校(毕业的学校)要求一切,不仅仅是100%的个人时间,是非常天真的),对于本篇文章来说,到目前为止,就没有丰富的题材了。一个备选主题是构造器的讨论,尤其是前沿的构造器,异常处理和双角色(two-stage)对象构造。另一个主题——再瞥一眼Yaslander技术[2]——创建不完全类型的容器(比如,lists, vectors, maps),凭借一些有趣的技巧完全可以实现,但是没有标准容器的保证。

当两个主题都很喜欢的时候,他们又不能给无锁数据结构(lock-free data structures)一个机会,这可是所有多线程编程社区的共同期望。在今年的PLDI会议(http://www.cs.umd.edu/~pugh/pldi04/)上,Michael Maged展示了世界上第一个无锁内存分配器[7],这,在很多测试上,超越了其他更复杂,精心设计的基于锁的分配器。这是不久前出现的许多无锁数据结构和算法的最新成果,……不过,让我们从头开始吧。
 
1 何谓无锁
这就是曾经我最想问一个问题。作为一名真正的多线程程序员,我最熟悉基于锁的多线程算法。在经典的基于锁的编程中,不论何时如果你需要共享一些数据,那么你需要串行访问它。那些改变数据的操作必须是原子的,这样没有其它的线程干扰,并破坏数据的不变性。即使像++count_,这样简单的操作,其中count_是整数类型,也必须加锁,因为++确实是一个三步操作,但是没有必要原子操作。
 
总之,在基于锁的多线程编程中,你需要保证对竞争条件很敏感的共享数据上的任何操作,都通过加锁或解锁一个独占(mutex)来实现原子操作。锁上独占是非常明智的,只要锁上独占之后,你就可以执行几乎任意的操作,推心置腹的说,没有线程会欺骗你的共享状态。
 
确切的来说,当一个独占被加锁之后你的操作是随机的,也是有问题。比如说,你可以,读取键盘输入或执行一个缓慢的I/O操作,这就意味着你延迟了其它任何等待同一个独占的线程。糟糕的是,你可以决定,你想去访问其它的一些共享数据,并试图锁上它的独占。如果另外一个线程已经锁上了最后一个独占,并且试图去访问你的线程持有的第一个独占,马上两个进程都被挂起,所以你可以称之为死锁
 
进入无锁编程。在无锁编程中你几乎不能以原子的方式做任何的事情。仅仅只有极少数的操作可以通过原子方式处理,这些限制使得无锁编程的方法更难了。(事实上,世界上大概有半打无锁编程专家,但是你真的不在此列。然而,庆幸的是,本文将给你提供基础的工具、参考书和热心,助你成为其中的一员)。作为这么一个稀有的框架的回报就是,你可以提供线程进度和线程之间的交互更好的保证。但是,无锁编程中你可以原子化处理的这些少量的事(small set of things)究竟是什么呢?事实上,允许实现任何无锁算法的最小的原子原语(atomic primitives)集是什么呢——如果存在这样一个集合?
 
如果你认为这是一个简单十足的问题,那有必要为它设立一个大奖?但是别人就这样干的。2003年,Maurice Herilihy因为其萌芽之作《无等待同步》而获得分布式计算领域的Edsger W.Dijkstra奖。(看看http://www.prodx.org/dijkstra/2003.html吧。其中还包括一个这篇论文的连接)。在这篇绝技(tour-de-force)论文中,Herlihy证明了建立无锁数据结构的好的和不好的原语。这促使了一个表面上很热火的硬件架构立即废弃,同时明晰未来的硬件中应该实现什么样的同步原语。
 
例如,在Herlihy的论文中给出的不可能的结果,表明原子操作,如,test-and-set(检测-设置)、swap(交换)、fetch-and-add(获取-增加)或即使是atomic queues(原子队列)都不适合同步两个以上的线程。(这实在让人感到惊讶,因为队列的原子pushpop操作应该提供非常强的抽象。)很精明的是,Herlihy还给出了通常的结果,证明了一些非常简单的结构足以实现支持任意数量线程的无锁算法。
 
最简单,最流行,并且贯穿本文的通用原语,是比较-交换(compare-and-swap,CAS)操作:
 
template <class T>
bool CAS(T* addr, T exp, T val)
{
       if (*addr == exp)
        {
                  *addr = val;
               return true;
      }
return false;
}
 
CAS用一个期望值来比较一个内存地址的内容,如果比较成功,则用一个新的值来替换这个内容(上下文)。这个过程是原子的。许多现代的处理器实现了CAS或等价不同位长的原语(为了更合理,我们把它作为template,假设一个实现采用元编程(metaprogramming)来约束可能的T)。一个让人称道的规则是,CAS能够原子的比较和交换的位越多,越容易用它来实现无锁数据结构。当今的32位处理器实现62位的CAS;比如,Intel的汇编器称之为CMPXCHG8(你将喜欢这些汇编器的助记术)。

---------------源文档------------------
Lock-Free Data Structures
Andrei Alexandrescu
December 17, 2007
http://erdani.org/publications/cuj-2004-10.pdf
最后一次访问时间:2007年12月30日
----------------------------------------