我正在阅读乔恩·斯凯特(Jon Skeet)对一个问题的回答,他在讲话中提到了这一点:
就我而言,无锁多线程是针对真正的线程专家的,我不是其中之一。
这不是我第一次听到此消息,但是如果您有兴趣学习如何编写无锁多线程代码,那么我发现很少有人谈论您的实际操作方式。
因此,我的问题除了学习关于线程的所有知识外,还从其他地方开始尝试专门编写无锁的多线程代码,以及哪些是很好的资源。
干杯
Answers:
当前的“无锁”实现大多数时候都遵循相同的模式:
(*可选:取决于数据结构/算法)
最后一点与自旋锁非常相似。实际上,这是一个基本的自旋锁。:)
我同意@nobugz的观点:无锁多线程中使用的互锁操作的成本由它必须执行的缓存和内存一致性任务决定。
但是,使用“无锁”的数据结构可以获得的好处是,您的“锁”的粒度非常细。这减少了两个并发线程访问同一“锁”(内存位置)的机会。
大多数时候,窍门是您没有专用的锁-而是将数组中的所有元素或链表中的所有节点都视为“自旋锁”。您已阅读,修改并尝试更新,如果自上次阅读以来没有更新。如果存在,请重试。
这使您的“锁定”(哦,对不起,非锁定:)非常精细,而没有引入额外的内存或资源需求。
使其更细粒度可减少等待的可能性。在不引入其他资源需求的情况下尽可能地细化听起来不错,不是吗?
但是,大多数乐趣来自确保正确的加载/存储排序。
与人们的直觉相反,CPU可以自由地对内存的读/写进行重新排序-顺便说一下,它们非常聪明:您将很难从单个线程观察到这一点。但是,当您开始在多个内核上执行多线程时,就会遇到问题。您的直觉将破裂:仅仅因为一条指令在您的代码中位于更早的位置,并不意味着它实际上会更早地发生。CPU可以不按顺序处理指令:它们特别喜欢对具有内存访问权限的指令执行此操作,以隐藏主内存延迟并更好地利用其缓存。
现在,可以肯定地说,代码序列不会“自上而下”地流动,而是像根本没有序列一样运行,并且可以称为“魔鬼的游乐场”。我认为就将要进行的加载/存储重新排序给出确切的答案是不可行的。取而代之的是,人们总是用力量,力量和罐头说话,并为最坏的情况做准备。“哦,CPU可能会在读取之前对读取进行重新排序,因此最好在此位置放置一个内存屏障。”
事情是由事实,即使是这些复杂的玉米和不妨可以在CPU架构不同。这可能是这种情况,例如,东西是保证不会发生在一个架构 可能会发生在另一个上。
要获得“无锁”多线程权限,您必须了解内存模型。
但是,正如这个故事MFENCE
所表明的那样,获取内存模型并保证正确性并非易事,英特尔和AMD对该文档进行了一些更正,从而引起了JVM开发人员的不满。事实证明,开发人员从一开始就依赖的文档最初并不是那么精确。
.NET中的锁会导致隐式的内存屏障,因此您可以安全地使用它们(大多数情况下,例如,请参见例如Joe Duffy-Brad Abrams-Vance Morrison在延迟初始化,锁,volatile和内存方面的出色表现:)(请务必点击该页面上的链接。)
作为额外的好处,您还会在边线探索中了解.NET内存模型。:)
万斯·莫里森(Vance Morrison)也提供了一个“老套可笑的故事”:每个开发人员必须了解的多线程应用程序。
...当然,正如@Eric所言,Joe Duffy是该主题的权威读物。
一个好的STM可以尽可能接近细粒度的锁定,并且可能提供与手工实现接近或相称的性能。其中之一是STM.NET从DevLabs项目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以及(虽然他们似乎没有为活动)。
乔·达菲的书:
http://www.bluebytesoftware.com/books/winconc/winconc_book_resources.html
他还撰写了有关这些主题的博客。
正确设置低锁程序的诀窍是深入了解确切的硬件,操作系统和运行时环境组合中的内存模型规则。
我个人还不够聪明,无法完成InterlockedIncrement之外的正确的低锁编程,但是如果您做的很好,那就去做吧。只需确保在代码中保留大量文档,以使那些不如您聪明的人不会意外破坏您的一个内存模型不变式并引入一个无法发现的错误。
这些天没有“无锁线程”之类的东西。上世纪末,当计算机硬件缓慢而昂贵时,这是一个学术界和类似组织感兴趣的游乐场。 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为您提供的工具并尝试实现自己的工具,您根本就无法领先。
atomic
块中。总而言之,在许多情况下,使用无锁结构可能同样棘手。
当涉及到多线程时,您必须确切地知道自己在做什么。我的意思是探讨在多线程环境中工作时可能发生的所有可能的场景/情况。无锁多线程不是我们合并的库或类,它是我们在线程旅途中获得的知识/经验。
即使在.NET中使用无锁线程可能会很困难,但在使用锁时,通常可以通过确切地研究需要锁定的内容并最小化锁定部分来进行重大改进……这也称为最小化锁定粒度。
例如,您只需要确保收集线程安全即可。如果它在每个项目上执行一些占用大量CPU的任务,则不要盲目地对遍历该集合的方法进行锁定。您可能只需要锁定创建集合的浅表副本即可。这样就可以在没有锁定的情况下遍历副本。当然,这在很大程度上取决于您代码的细节,但是我已经能够使用这种方法解决锁车队的问题。