无锁多线程是真正的线程专家


86

我正在阅读乔恩·斯凯特Jon Skeet)对一个问题的回答,他在讲话中提到了这一点:

就我而言,无锁多线程是针对真正的线程专家的,我不是其中之一。

这不是我第一次听到此消息,但是如果您有兴趣学习如何编写无锁多线程代码,那么我发现很少有人谈论您的实际操作方式。

因此,我的问题除了学习关于线程的所有知识外,还从其他地方开始尝试专门编写无锁的多线程代码,以及哪些是很好的资源。

干杯


我使用gcc,linux和X86 / X68平台。无锁并不像它们都听起来那么难!gcc原子内建函数在intel上具有内存障碍,但这在现实生活中并不重要。重要的是内存是原子修改的。当您设计“无锁”数据结构时,它只是震撼了,而当另一个线程看到更改时,这无关紧要。单个链接列表,跳过列表,哈希表,空闲列表等都非常容易实现无锁。无锁并不代表一切。它只是适用于某些情况的另一个工具。
johnnycrash 2012年


投票关闭以作为资源推荐,或者不清楚您的要求。
西罗Santilli郝海东冠状病六四事件法轮功

Answers:


100

当前的“无锁”实现大多数时候都遵循相同的模式:

  • *阅读并复制一份状态**
  • *修改副本**
  • 进行联锁操作
  • 如果失败重试

(*可选:取决于数据结构/算法)

最后一点与自旋锁非常相似。实际上,这是一个基本的自旋锁。:)
我同意@nobugz的观点:无锁多线程中使用的互锁操作的成本由它必须执行的缓存和内存一致性任务决定

但是,使用“无锁”的数据结构可以获得的好处是,您的“锁”的粒度非常细。这减少了两个并发线程访问同一“锁”(内存位置)的机会。

大多数时候,窍门是您没有专用的锁-而是将数组中的所有元素或链表中的所有节点都视为“自旋锁”。您已阅读,修改并尝试更新,如果自上次阅读以来没有更新。如果存在,请重试。
这使您的“锁定”(哦,对不起,非锁定:)非常精细,而没有引入额外的内存或资源需求。
使其更细粒度可减少等待的可能性。在不引入其他资源需求的情况下尽可能地细化听起来不错,不是吗?

但是,大多数乐趣来自确保正确的加载/存储排序
与人们的直觉相反,CPU可以自由地对内存的读/写进行重新排序-顺便说一下,它们非常聪明:您将很难从单个线程观察到这一点。但是,当您开始在多个内核上执行多线程时,就会遇到问题。您的直觉将破裂:仅仅因为一条指令在您的代码中位于更早的位置,并不意味着它实际上会更早地发生。CPU可以不按顺序处理指令:它们特别喜欢对具有内存访问权限的指令执行此操作,以隐藏主内存延迟并更好地利用其缓存。

现在,可以肯定地说,代码序列不会“自上而下”地流动,而是像根本没有序列一样运行,并且可以称为“魔鬼的游乐场”。我认为就将要进行的加载/存储重新排序给出确切的答案是不可行的。取而代之的是,人们总是用力量力量罐头说话,并为最坏的情况做准备。“哦,CPU可能会在读取之前对读取进行重新排序,因此最好在此位置放置一个内存屏障。”

事情是由事实,即使是这些复杂的玉米不妨可以在CPU架构不同。这可能是这种情况,例如,东西是保证不会发生在一个架构 可能会发生在另一个上。


要获得“无锁”多线程权限,您必须了解内存模型。
但是,正如这个故事MFENCE所表明的那样,获取内存模型并保证正确性并非易事,英特尔和AMD对该文档进行了一些更正,从而引起了JVM开发人员的不满。事实证明,开发人员从一开始就依赖的文档最初并不是那么精确。

.NET中的锁会导致隐式的内存屏障,因此您可以安全地使用它们(大多数情况下,例如,请参见例如Joe Duffy-Brad Abrams-Vance Morrison在延迟初始化,锁,volatile和内存方面的出色表现:)(请务必点击该页面上的链接。)

作为额外的好处,您还会在边线探索中了解.NET内存模型。:)

万斯·莫里森(Vance Morrison)也提供了一个“老套可笑的故事”:每个开发人员必须了解的多线程应用程序

...当然,正如@Eric所言Joe Duffy是该主题的权威读物

一个好的STM可以尽可能接近细粒度的锁定,并且可能提供与手工实现接近或相称的性能。其中之一是STM.NETDevLabs项目MS的。

如果您不是仅使用.NET的狂热者,Doug Lea会在JSR-166中做一些出色的工作
Cliff Click对哈希表有一个有趣的看法,该哈希表不像Java和.NET并发哈希表那样依赖锁条,并且似乎可以很好地扩展到750个CPU。

如果您不怕冒险进入Linux领域,那么下面的文章将提供有关当前内存体系结构内部以及高速缓存行共享如何破坏性能的更多见解:每个程序员都应该了解内存

@Ben对MPI发表了许多评论:我衷心同意MPI可能在某些领域大放异彩。与尝试精明的半熟式锁定实现相比,基于MPI的解决方案更容易推论,更易于实现且出错率更低。(但是-从主观上讲-对于基于STM的解决方案也是如此。)我也敢打赌,正确写出体面的语言要容易很多年。,正如许多成功的例子所表明的那样,用Erlang分布式应用程序。

但是,当在单个多核系统上运行MPI时,它会有自己的成本和麻烦。例如,在Erlang中,围绕流程调度和消息队列同步有待解决的问题。
同样,MPI系统的核心通常是为“轻量级进程”实现一种协作的N:M调度。例如,这意味着在轻量级进程之间不可避免的上下文切换。的确,它不是“经典上下文切换”,而是大部分是用户空间操作,并且可以快速执行-但是,我真诚地怀疑,是否可以将其置于互锁操作需要20-200个周期内。用户模式上下文切换肯定较慢即使在Intel McRT库中。轻量级进程的N:M计划并不是什么新鲜事。LWP在Solaris中存在了很长时间。他们被遗弃了。在NT中有纤维。他们现在大部分都是遗物。NetBSD中存在“激活”。他们被遗弃了。Linux对N:M线程有自己的看法。到现在似乎已经死了。
有时会有新的竞争者:例如Intel的McRT或最近与Microsoft的ConCRT一起进行的用户模式调度。 在最低级别上,它们执行N:M MPI调度程序的工作。Erlang-或任何MPI系统-通过利用新的特性,可能会在SMP系统上大大受益
UMS

我想OP的问题不是关于/针对任何解决方案的优缺点和主观论点,但如果我必须回答,我想它取决于任务:构建在A上运行的低级别,高性能基本数据结构具有低核心/低锁定/“无锁定”技术或STM的单个系统将在性能方面产生最佳结果,并且即使在消除上述缺陷的情况下,也可能会在性能方面随时击败MPI解决方案。例如在Erlang中。 对于构建可以在单个系统上运行的中等复杂程度的东西,我可能会选择经典的粗粒度锁定,或者如果性能非常受关注,则选择STM。 对于构建分布式系统,MPI系统可能是自然选择。


请注意,有MPI实现.NET以及(虽然他们似乎没有为活动)。


1
虽然这个答案有很多有用的信息,但无标题的算法和数据结构本质上只是非常细粒度的自旋锁的集合这一大标题想法是错误的。虽然通常会在无锁结构中看到重试循环,但是行为却大不相同:锁(包括自旋锁)专门获取一些资源,而其他线程在持有该资源时无法取得进展。从这个意义上说,“重试”只是在等待释放独占资源。
BeeOnRope

1
另一方面,无锁算法不使用CAS或其他原子指令来获取专有资源,而是完成某些操作。如果失败,则是由于与另一个线程在时间上进行了细粒度的竞争,在这种情况下,另一个线程取得了进展(已完成其操作)。如果无限期地怀疑某个线程,则所有其他线程仍然可以取得进展。从质量和性能上来说,这与排他锁有很大不同。即使在激烈竞争下,大多数CAS循环的“重试”次数也通常很低...
BeeOnRope

1
...但是,这当然并不意味着良好的扩展性:即使由于CAS失败的次数不多,由于内核之间的插槽间延迟,单个存储器位置的争用在SMP机器上总是相当缓慢。低。
BeeOnRope

1
@AndrasVass-我猜这也取决于“好”与“坏”的无锁代码。当然,任何人都可以编写结构并将其称为无锁结构,而实际上它仅使用用户模式自旋锁,甚至不满足定义。我也鼓励任何有兴趣的读者阅读Herlihy和Shavit的这篇论文,以正式的方式看待基于锁和无锁算法的各种类别。建议您阅读Herlihy关于该主题的任何内容。
BeeOnRope

1
@AndrasVass-我不同意。大多数经典的无锁结构(列表,队列,并发映射等)甚至对于共享的可变结构都没有旋转,例如在Java中,相同的实际现有实现遵循相同的模式(我不是熟悉本机编译的C或C ++中提供的功能,并且由于没有垃圾回收而很难实现)。也许您和我对旋转有不同的定义:我不认为您在无锁产品“旋转”中发现的“ CAS重试”。IMO的“旋转”意味着热烈等待。
BeeOnRope

27

乔·达菲的书:

http://www.bluebytesoftware.com/books/winconc/winconc_book_resources.html

他还撰写了有关这些主题的博客。

正确设置低锁程序的诀窍是深入了解确切的硬件,操作系统和运行时环境组合中的内存模型规则。

我个人还不够聪明,无法完成InterlockedIncrement之外的正确的低锁编程,但是如果您做的很好,那就去做吧。只需确保在代码中保留大量文档,以使那些不如您聪明的人不会意外破坏您的一个内存模型不变式并引入一个无法发现的错误。


38
因此,如果埃里克·利珀特Eric Lippert)乔恩·斯凯特Jon Skeet)都认为无锁编程只适合比自己聪明的人,那么我将谦虚地逃避这一想法。;-)
dodgy_coder 2012年

20

这些天没有“无锁线程”之类的东西。上世纪末,当计算机硬件缓慢而昂贵时,这是一个学术界和类似组织感兴趣的游乐场。 Dekker的算法始终是我最喜欢的算法,现代硬件已将它推广到牧场。它不再起作用了。

两种发展结束了这一点:RAM和CPU速度之间的差距越来越大。芯片制造商具有在一个芯片上放置多个CPU内核的能力。

RAM速度问题要求芯片设计人员在CPU芯片上放置一个缓冲区。缓冲区存储代码和数据,可由CPU内核快速访问。并且可以以低得多的速率在RAM中进行读写。此缓冲区称为CPU缓存,大多数CPU至少有两个。第一级高速缓存小而又快,第二级高速缓存又大又慢。只要CPU可以从一级缓存读取数据和指令,它就会快速运行。高速缓存未命中确实非常昂贵,如果数据不在第一个高速缓存中,它将使CPU休眠多达10个周期;如果不在第二级高速缓存中并且需要从中读取数据,它将使CPU休眠多达200个周期。内存。

每个CPU内核都有自己的缓存,它们存储自己的RAM“视图”。当CPU写入数据时,将写入高速缓存,然后将其缓慢刷新到RAM。不可避免地,每个内核现在将对RAM内容有不同的看法。换句话说,一个CPU在该RAM写入周期完成并且CPU刷新其自己的视图之前,不知道另一个CPU写入了什么。

这与线程明显不兼容。当您必须读取另一个线程写入的数据时,您始终会真正关心另一个线程的状态。为了确保这一点,您需要显式编程一个所谓的内存屏障。它是低级CPU原语,可确保所有CPU缓存处于一致状态并具有最新的RAM视图。所有挂起的写入都必须刷新到RAM,然后需要刷新缓存。

这在.NET中可用,Thread.MemoryBarrier()方法实现一个。鉴于这是lock语句完成的工作的90%(以及执行时间的95 +%),因此避免使用.NET为您提供的工具并尝试实现自己的工具,您根本就无法领先。


2
@ Davy8:组成仍然很难。如果我有两个无锁的哈希表,并且作为使用者我可以访问它们两者,那么这将不能保证整个状态的一致性。今天最接近您的是STM,您可以在其中将两个访问权限放在一个atomic块中。总而言之,在许多情况下,使用无锁结构可能同样棘手。
安德拉斯·瓦斯

4
我可能是错的,但是我认为您已经错误地解释了缓存一致性的工作原理。大多数现代多核处理器都具有一致的缓存,这意味着缓存硬件将通过阻塞“读取”调用直到所有相应的“写入”调用完成来确保所有进程对RAM内容具有相同的视图。Thread.MemoryBarrier()文档(msdn.microsoft.com/en-us/library/…)根本没有提及缓存行为-这只是一个指令,可以防止处理器对读取和写入进行重新排序。
Brooks Moses

7
“如今没有“无锁线程”之类的东西。” 告诉Erlang和Haskell程序员。
朱丽叶

4
@HansPassant:“现在没有像“无锁线程”这样的东西了。F#,Erlang,Haskell,Cilk,OCaml,Microsoft的任务并行库(TPL)和Intel的线程构件块(TBB)都鼓励无锁多线程编程。这些天,我很少在生产代码中使用锁。
JD

5
@HansPassant:“所谓的内存屏障。它是一种低级CPU原语,可确保所有CPU缓存处于一致状态并具有最新的RAM视图。所有挂起的写操作必须刷新到RAM,然后需要刷新缓存”。在这种情况下,内存屏障可防止编译器或CPU对内存指令(加载和存储)进行重新排序。与CPU缓存的一致性无关。
JD


0

当涉及到多线程时,您必须确切地知道自己在做什么。我的意思是探讨在多线程环境中工作时可能发生的所有可能的场景/情况。无锁多线程不是我们合并的库或类,它是我们在线程旅途中获得的知识/经验。


有许多库提供无锁线程语义。STM特别令人感兴趣,其中有许多实现方式。
Marcelo Cantos

我看到这两个方面。要从无锁的库中获得有效的性能,需要对内存模型有深入的了解。但是,不具备这些知识的程序员仍然可以从正确性优势中受益。
Ben Voigt

0

即使在.NET中使用无锁线程可能会很困难,但在使用锁时,通常可以通过确切地研究需要锁定的内容并最小化锁定部分来进行重大改进……这也称为最小化锁定粒度

例如,您只需要确保收集线程安全即可。如果它在每个项目上执行一些占用大量CPU的任务,则不要盲目地对遍历该集合的方法进行锁定。您可能只需要锁定创建集合的浅表副本即可。这样就可以在没有锁定的情况下遍历副本。当然,这在很大程度上取决于您代码的细节,但是我已经能够使用这种方法解决锁车队的问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.