线程安全的无锁算法

原著:Patrick Garrity ‘12 – St. OlafCollege – [email protected]

原文:http://www2.css.edu/mics/Submissions/submissions/Lock-Free%20Algorithms%20for%20Thread%20Safe%20Programming.pdf

翻译:CoryXie <[email protected]>

 

摘要:一个对象是无锁的,只要它能保证在系统中的操作该对象的多个线程中的“一些”线程能够在有限的步骤内完成对该对象的操作,不管其他线程发生了什么。更实际的说,无锁编程是无需使用锁(这是目前的标准方法)来实现线程安全的技术。无锁对象(LFO)有许多有用的属性,使他们比有锁对象的应用更广泛,往往在高争用情况下表现很好(即由多个线程同时访问)。虽然无锁对象目前不是很常见,但是在实践中还是有机会改变的。无锁算法(LFA)出了名的难以设计,也难以证明是正确的。同时在实现过程中,也提出了一系列的技术难题,但在过去十年左右的过程中几种常用的数据结构已经被开发出来。本文从实际情况来讨论无锁对象。分析了无锁对象的特点和存在的问题,以及具体实现问题。本文还总结了一大套有用的无锁算法,这将是可以广泛使用的。最后,报告作者实现一组用于一般用途的无锁数据结构(LFDS)的努力。

 

1.    介绍

 

自从20世纪90年代之前,研究人员已经开发出了无锁算法,尽管很多实际有用的算法没有公布,直到过去的十年。多核计算机的兴起和其所承诺的高并发系统为高效并行算法提供了市场。LFA在这个市场中是特别理想的,因为他们往往​​在高争用情形下表现非常好。

 

具体而言,大多数关于无锁的研究都是有针对性地设计算法,用以可扩展的无锁数据结构,这在并发访问的情况下提供能许多有用的属性。从定义而言,LFDS是死锁和活锁免疫的,这对任何开发人员都是非常有用的 [12]。此外,在任意线程失败的情况下,他们也能保持这种属性——任意数量的线程都可能会被杀死,而LFDS将继续维持其功能性 [12]。 LFDS也能容忍优先级反转,因为他们不会让一个低优先级的线程失去任何东西(锁)[12]。同样的,他们也能容忍线程抢占。在所有这些情况下,基于锁的数据结构可能遭受问题或停止运作。最后,LFDS有异步信号安全性,这使得它们可以安全地用于信号处理程序中 [12]。在信号处理程序中使用锁通常是不安全的,因为这可能使得处理器和线程处于死锁状态。这就进一步阻止了许多实现的malloc()和free()在信号处理程序中被使用,因为它们是基于锁的。

 

2.    问题和解决方案

 

尽管LFDS具有令人印象深刻的功能特性,他们也遭受了一些问题,从而阻碍了他们成为一个强大的主流存在。首先,LFDS难以设计,难以证明其正确性。此外,实现这些算法也是困难的,提出了一系列技术难题和系统依赖问题,其中一些在第4节会有针对性的讨论。在此之外,有三个主要的问题影响LFDS:ABA问题,不降低内存使用,以及内存分配。在写作本文的时候,所有这些问题都已经有了解决方案。

 

ABA问题发生在一个进程已经读取了共享内存值A后,被另一个进程抢占。第二个进程将共享内存值由A改变到B,然后在第一个进程继续之前再将其返回到A。一旦第一个进程恢复执行,它没有注意到A已经改变,从而认为这是真实的。这就导致一种问题,无锁对象在一个线程中删除了一些值,然后,另一个线程在相同的位置又分配一个新值。在这种情况下,第一个线程将有一个指针没有改变,但该指针指向的值已经被改变了。 ABA问题的解决方案,包括标记(tagging),垃圾收集(garbage collection)或类似的过程。标记涉及使用空闲未用的指针位,来对指针被修改或使用的次数进行计数[11]。这初步解决了ABA问题,但是计数器可以回绕到与旧指针持有相同的值。ABA问题的解决方案的更完备的证明是使用垃圾收集或类似的策略来做内存管理[11]。虽然不是作为内存管理的目标存在,这种垃圾收集机制却顺带解决了ABA问题。由于垃圾收集不是所有系统都支持,对于一些语言(如C)就要求有另一种解决方案。

 

这直接导致LFDS不能降低(non-decreasing)内存使用的问题。这是由于无法从LFDS物理删除任何数据。数据可以在逻辑上被断开,但也没有办法来确定是否有另一个线程正在访问被删除的数据。物理删除该数据,可能会导致段错误(segmentation faults)。其结果是,LFDS将继续消耗内存,直到没有内存剩下,即便数据已经从中删除。

 

垃圾收集解决了这个问题,但一般是不提供的。对这个问题的最知名的解决方案是一种被称为风险指针(Hazard Pointers)的算法,由Michael开发 [10,11]。该算法依赖于一个全局指针列表,其中包含所有线程都可以访问的指向数据的指针。当一个线程在获得指向有效数据的指针时,该指针被放置在这个全局列表中。每个线程还都保持一份希望物理删除的数据的指针列表。一旦这个本地列表达到一定的阈值大小,每一个不属于全局列表成员的指针都可以被释放[11]。这种手动的垃圾回收方式有效地解决了内存使用的问题,作为一个(好的)副作用,还可以防止ABA问题[11]。【译注:下图展示了这段话的原理。】


                                                图片来源:http://www.concurrencykit.org/doc/ck_hp.html

 

内存分配的问题源于一个事实,即大多数的malloc实现(或其他内存分配器)不是无锁的。因此,使用这些分配器的LFDS也不算是完全无锁的。这个问题要求实现无锁分配器,该算法的确存在[12]。关于这些算法是否执行得更好或与有锁实现同等,尚有一些争论。

 

最后,也应该提到,无锁编程也有性能的问题。(有些情况下)所使用的原子操作可能会比用锁的代价还要昂贵,可能还有内存屏障,使用太多可能会导致整体性能下降。在设计这些算法时需要特别注意,以尽可能地减少或消除这些调用。这种降低也是体系结构相关的,不同的核心/缓存(core/cache)设置可能会产生不同的表现。其他问题暂且不论,无锁算法的实现还需要额外的洞察力。

 

3.     算法概述

 

在过去的二十年中,无锁算法研究工作取得了较大进展。本文探讨了LFDS的进展,他们广泛应用于并发代码的背景下。

 

3.1  Stacksand Queues

 

目前,存在各种堆栈算法,且堆栈可以说是最简单的LFDS设计和实现。其中一个最古老的无锁堆栈算法是IBM研究院提出,基于传统的堆栈实现。这种实现风格经历了时间的考验,被作为无锁堆栈的设计起点。Michael还设计了一个类似的堆栈,他应用了风险指针(Hazard Pointers),使得它是第一个支持内存回收的LFDS。但是,这两个堆栈算法都不理想,因为只有单一的访问点,即堆栈顶部。由于LFDS被设计成在高并发的情况下使用,这些堆栈可能成为瓶颈。最近,研究人员设计了更具扩展性的算法,试图管理堆栈顶部的争用。其中之一,由Hendler, Shavit, 和 Yerushalmi提出,通过跟踪每个线程正在执行的堆栈操作(PUSH,POP,TOP),简化了内部调用[2]。本文作者还独立实现了一个堆栈,虽然它与Michael展示的算法一样。

 

队列实现稍微复杂一些,但仍然可以利用有限数量的访问点。有一个在1996年由Michael 和 Scott设计的算法,也已经由本文作者实现 [8]。曾经有一段时间英特尔的线程构建块(Intel ThreadingBuilding Blocks)软件包尝试用无锁算法来实现他们的并发队列类。然而,测试表明,原子的CAS操作对性能成本影响还是太严重[16]。

 

3.2  Lists,Skip Lists, and Hash Tables

 

(单)链表算法明显比堆栈或队列更复杂,因为他们具有任意的插入和删除操作的可能性。这一类的LFDS已经被大量研究,但不是所有建议的设计都可行。例如,一些链表的算法基于DCAS(双字比较和交换),这是一般的体系结构不提供的。Fomitchev和Ruppert于2004年提出了一个实际的链表算法,依赖于CAS,结合使用两个标志位[13]。原来的解决方案是基于有序键值(ordered keys)的列表(其中存储列表值),但由本文作者进行了改进和泛化[13]。这种泛化支持任何类型的数据,不讲究顺序,还删除了对键值的依赖——列表中只存储值。

 

链表的延伸是跳表(skiplist)。跳表是有序的链表,其中每个节点分配一个随机的高度,这个高度可以用于在搜索时跳过节点。跳表以分摊时间复杂度O(log(N))执行,使他们在如字典类结构中很有用。在无锁领域,跳表被用来代替二叉搜索树,因为直到最近(2010年)之前一直不存在良好的BST算法。在2004年,Fomitchev和Ruppert在论述他们的链表算法时,顺带提出了跳表算法[13]。此外,Fraser, Sundell, 以及 Tsigas曾致力于开发无锁跳表算法。

 

这是直接与哈希表相关的,它依赖于集合(sets)提供桶(buckets)。到目前为止,使用基于列表的集合(跳表)已经实现了哈希表算法,虽然现在可能也可以使用二叉搜索树来创建哈希表。在2002年,Michael提出了一个无锁哈希表算法,依赖于[8]中提出的基于列表的集合。

 

3.3  BinarySearch Trees and Deques

 

在无锁领域的最新贡献,是一个很好的二叉搜索树算法。 2010年,Ellen,Ruppert,Fatourou 和 Van Breugel提出了基于叶的树(leaf-based tree),实现了一个字典(每个叶子节点是一个键值对)[5]。其它相似的算法也已经被开发出来(如前一节中提到的),但不利用树作为其主要结构。例如,Sundell和Tsigas提出的无锁字典[6],依赖于跳表。最后,大量的工作已被用于开发双端队列(deques),环形列表(circular lists),和相关的数据结构。这种努力的一个最好的例子是Chase 和 Lev 在[3]中所展示的。

 

4.     实现细节

 

无锁算法通常依赖于一个特殊的处理器指令,称为比较和交换,或CAS。此操作是许多实现中的公共点,为了在当前的无锁算法(LFA)领域工作,必须理解这一点。CAS是一个原子操作,当且仅当它是某个预期值的情况下才可以改变一个值。 CAS指令可以通过下面的C代码来描述:

 

比较和交换的强处是在一个单一的指令中实现了多个操作。由于CAS是一个处理器指令,这种行为是在没有任何锁的情况下实现的。这种原子性,可以允许聪明的应用来替换锁操作。一个例子是,用CAS弹出栈顶。清单4.2试图这样做,先寻找堆栈的顶部,取得下一个元素,然后在堆栈的顶部上做CAS操作。

列表4.2考虑了并发操作——如果堆栈顶部在第3和5行之间被改变了,CAS调用将会失败。由于CAS是一个原子操作,如果它的调用开始,它能保证不被中断地完成。

 

CAS指令在大多数现代32 位和64位系统上都是可用的,因此是组成这些算法一个可靠的部分。但是,使用此指令可能依赖于(软件)平台。GCC, Mac OS X, 和 Windows库都提供了CAS函数,尽管每个的行为都不同。此外,对于不提供CAS的系统,该指令必须通过内联汇编来使用。这种情况使得在所有平台上实现一个通用的CAS函数,也是一个不平凡的任务。

 

作为一种CAS替代方法,LL / SC指令(load-linked/store conditional)也被使用。这是一个实现细节,但具有相同的效果。几个现有无锁算法的一个缺陷是使用DCAS,在两个不相邻的字上同时执行CAS操作。此操作可能是有用的,但一般的硬件上不支持。如果超过了单指针需要一次进行比较和交换,必须使用其他的方法。携带附加数据的方法之一是通过使用空余指针位。在32位系统中,可以有2位未使用。在64位系统中,可能16位都可以访问。管理多余的指针位可能是一个冒险的解决方案,但它也提供了算法实现的解决方案,不用诉诸可能不被支持的(硬件)功能。

 

还有一些实现无锁算法的努力,通过尽量减少使用CAS和原子原语。这些努力通常会导致高吞吐量的无等待(wait-free)的实现(无锁的加强版——所有线程都能取得进展)[1]。

 

5.库的开发

 

在过去的数月内,本文作者开发了一个无锁数据结构库。这个库用C ++实现,在可能的情况下尽可能和标准模板库中的数据结构对应。经过几个迭代周期,完成了可工作的堆栈,队列,和链表。此外,实现了灵活的风险指针(hazard pointers)系统,可以很容易地被用来管理任何LFDS的风险指针。堆栈和队列被内存管理,而列表还是一个进展中的工作。最后,二叉搜索树和字典的实现也正在开发中。这个库是跨平台的,在GCC 4.4+和Visual Studio 2010的32位和64位系统下都可编译。

 

不幸的是,这个项目无法向公众开放,被保持作为一个研究项目。该项目的最初目标是建立一个强大的,开放源码的模板库。由于目前正在研究某些算法的专利问题,源码不能在这个时候被释放出来。至少有两个成长中的C库以及开源社区,正在朝这个方向努力。

 

6.结论

 

目前,在过去大致二十年中,已公布了一套丰富的无锁算法。无锁代码虽然有一些固有的问题,但是大多已得到解决或以某种方式得以纠正。此外,无锁对象有很大的好处,比有锁算法使用范围更广泛,性能代价更好。即便如此,这些算法的使用一直没有标准,实现也较为分散。人们已经设计了越来越多的无锁代码库,本文作者在这方面也取得了进展。

 

7.参考资料

 

[1] Alex Kogan and Erez Petrank. 2011. Wait-free queues with multipleenqueuers and dequeuers. In Proceedings of the 16th ACM symposium on Principlesand practice of parallel programming (PPoPP '11). ACM, New York, NY, USA,223-234. DOI=10.1145/1941553.1941585http://doi.acm.org/10.1145/1941553.1941585

 

[2] Danny Hendler, Nir Shavit, and Lena Yerushalmi. 2004. A scalablelock-free stack algorithm. In Proceedings of the sixteenth annual ACM symposiumon Parallelism in algorithms and architectures (SPAA '04). ACM, New York, NY,USA, 206-215. DOI=10.1145/1007912.1007944http://doi.acm.org/10.1145/1007912.1007944

 

[3] David Chaseand Yossi Lev. 2005. Dynamic circular work-stealing deque. In Proceedings ofthe seventeenth annual ACM symposium on Parallelism in algorithms andarchitectures (SPAA '05). ACM, New York, NY, USA, 21-28.DOI=10.1145/1073970.1073974http://doi.acm.org/10.1145/1073970.1073974

 

[4] Dmitri Perelman, Rui Fan, and Idit Keidar. 2010. On maintainingmultiple versions in STM. InProceeding of the 29th ACM SIGACT-SIGOPSsymposium on Principles of distributed computing(PODC '10). ACM, New York,NY, USA, 16-25. DOI=10.1145/1835698.1835704 http://doi.acm.org/10.1145/1835698.1835704

 

[5] Faith Ellen, Panagiota Fatourou, Eric Ruppert, and Franck van Breugel.2010. Non-blocking binary search trees. InProceeding of the 29th ACMSIGACT-SIGOPS symposium on Principles of distributed computing(PODC '10).ACM, New York, NY, USA, 131-140. DOI=10.1145/1835698.1835736

http://doi.acm.org/10.1145/1835698.1835736

 

[6] Hakan Sundell and Philippas Tsigas. 2004. Scalable and lock-freeconcurrent dictionaries. InProceedings of the 2004 ACM symposium on Appliedcomputing (SAC '04). ACM, New York, NY, USA, 1438-1445.DOI=10.1145/967900.968188

http://doi.acm.org/10.1145/967900.968188

 

[7] M. Herlihy. 1990. A methodology for implementing highly concurrentdata structures.SIGPLAN Not. 25, 3 (February 1990), 197-206.DOI=10.1145/99164.99185 http://doi.acm.org/10.1145/99164.99185

 

[8] Maged M. Michael and Michael L. Scott. 1995. Simple, Fast, andPractical Non-Blocking and Blocking Concurrent Queue Algorithms. TechnicalReport. University of Rochester, Rochester, NY, USA.

 

[9] Maged M. Michael. 2002. High performance dynamic lock-free hash tablesand list-based sets. InProceedings of the fourteenth annual ACM symposiumon Parallel algorithms and architectures(SPAA '02). ACM, New York, NY,USA, 73-82. DOI=10.1145/564870.564881 http://doi.acm.org/10.1145/564870.564881

 

[10] Maged M.Michael. 2002. Safe memory reclamation for dynamic lock-free objects usingatomic reads and writes. InProceedings of the twenty-first annual symposiumon Principles of distributed computing(PODC '02). ACM, New York, NY, USA,21-30. DOI=10.1145/571825.571829

http://doi.acm.org/10.1145/571825.571829

 

[11] Maged M.Michael. 2004. Hazard Pointers: Safe Memory Reclamation for Lock-Free Objects.IEEETrans. Parallel Distrib. Syst. 15, 6 (June 2004), 491-504. DOI=10.1109/TPDS.2004.8http://dx.doi.org/10.1109/TPDS.2004.8

 

[12] Maged M.Michael. 2004. Scalable lock-free dynamic memory allocation. SIGPLAN Not. 39,6 (June 2004), 35-46. DOI=10.1145/996893.996848

http://doi.acm.org/10.1145/996893.996848

 

[13] MikhailFomitchev and Eric Ruppert. 2004. Lock-free linked lists and skip lists. InProceedingsof the twenty-third annual ACM symposium on Principles of distributed computing(PODC '04). ACM, New York, NY, USA, 50-59. DOI=10.1145/1011767.1011776http://doi.acm.org/10.1145/1011767.1011776

 

[14] Simon Doherty, Maurice Herlihy, Victor Luchangco, and Mark Moir.2004. Bringing practical lock-free synchronization to 64-bit applications. InProceedingsof the twenty-third annual ACM symposium on Principles of distributed computing(PODC '04). ACM, New York, NY, USA, 31-39. DOI=10.1145/1011767.1011773

http://doi.acm.org/10.1145/1011767.1011773

 

[15] Woongki Baek, Nathan Bronson, Christos Kozyrakis, and Kunle Olukotun.2010. Implementing and evaluating nested parallel transactions in softwaretransactional memory. InProceedings of the 22nd ACM symposium onParallelism in algorithms and architectures(SPAA '10). ACM, New York, NY,USA, 253-262. DOI=10.1145/1810479.1810528 http://doi.acm.org/10.1145/1810479.1810528

 

[16] WooyoungKim. The Concurrent Queue Container With Sleep Support. Intel Software NetworkBlogs. June 3, 2008. http://software.intel.com/en-us/blogs/2008/06/03/the-concurrent-queue-container-with-sleep-support/.

你可能感兴趣的:(线程安全)