“Lock-free”多线程编程是专为真正的多线程专家设计的。
“Lock-free”多线程编程是专为真正的多线程专家设计的。
我正在阅读Jon Skeet为一个问题提供的答案,在其中他提到了以下内容:
就我个人而言,无锁多线程编程是真正的多线程专家处理的事情,而我并不是其中的一员。
这不是我第一次听到这个说法,但我发现很少有人谈论如何实际做到这一点,如果您想学习如何编写无锁多线程代码,那么该从哪里开始学习,哪些资源是最好的呢?
干杯
乔·达菲的书:
http://www.bluebytesoftware.com/books/winconc/winconc_book_resources.html
他还在这些主题上写博客。
正确实现低锁定程序的技巧是深入理解内存模型的规则,特别是在您特定的硬件、操作系统和运行时环境组合中。
我个人不够聪明,无法在InterlockedIncrement之外进行正确的低锁定编程,但如果您可以,请继续前进。只需确保在代码中留下大量文档,以便不如您聪明的人不会意外破坏您的内存模型不变量并引入无法找到的错误。
目前的“无锁”实现大多数时间都遵循相同的模式:
- 读取一些状态并复制它*
- 修改复制品*
- 执行原子操作
- 如果失败则重试
(*可选:取决于数据结构/算法)
最后一步与自旋锁非常相似。事实上,它就是基本的自旋锁。:)
我同意@nobugz的观点:在无锁多线程中使用的原子操作的成本主要由缓存和内存一致性任务支配。
然而,通过使用一个“无锁”数据结构,你获得的好处是你的“锁”非常细粒度。这降低了两个并发线程访问相同“锁”(内存位置)的机会。
大多数情况下的技巧是你没有专用锁定,而是把例如数组中的所有元素或链接列表中的所有节点都视为“自旋锁”。你读取、修改并在自上次读取后没有更新时尝试更新。如果有更新,则重试。
这使得你的“锁定”(哦,抱歉,非锁定)非常细粒度,而不会引入额外的内存或资源要求。
将它变得更细粒度会降低等待的概率。使它尽可能细粒度而不引入额外的资源要求听起来很好,不是吗?
然而,最有趣的大多数情况下来自于确保正确的加载/存储顺序。
与人们的直觉相反,CPU可以重新排序内存读/写——顺便说一句:它们非常聪明,你很难从单个线程中观察到这一点。但是,当你在多个核心上进行多线程操作时,你会遇到问题。你的直觉会崩溃:仅因为一个指令在你的代码中更早,并不意味着它实际上会更早地发生。CPU可以无序处理指令:它们特别喜欢对具有内存访问的指令进行这样的操作,以隐藏主存储器的延迟并更好地利用它们的高速缓存。
现在,肯定违反直觉的是,一系列代码并不是“自上而下”流动,而是像没有序列一样运行,并可能被称为“魔鬼的游戏区”。我认为无法给出关于哪些加载/存储重排列会发生的确切答案。相反,我们总是用may和might和can相提并论,为最坏情况做准备。 “哦,CPU可能会重排这个读取在那个写入之前,所以最好在这个位置上放置一个屏障。”
事情的复杂性在于即使这些可能性也可能会因CPU架构而异。例如,有些在一种架构中保证不会发生的事情在另一种架构中可能会发生。
要想正确地实现“无锁”多线程,您必须了解内存模型。然而,正确地获得内存模型和保证并不是一件轻松的事情,正如这篇文章所示,英特尔和AMD对MFENCE
的文档进行了一些更正,引起了JVM开发人员的一些骚动。事实证明,开发人员从一开始就依赖的文档并不是那么精确。
.NET中的锁导致隐式内存屏障,因此使用它们是安全的(大多数情况下是这样的…例如,在Joe Duffy - Brad Abrams - Vance Morrison greatness关于惰性初始化、锁、挥发性和内存屏障的文章中。:)(一定要在该页面上查看链接)。
作为附加奖励,您还将在副本中介绍.NET内存模型。 🙂
还有一篇经典的作品来自Vance Morrison:每个Dev必须了解的多线程应用程序。
当然,正如@Eric所提到的,Joe Duffy是这个主题上的终极读物。
好的STM可以接近精细级锁定,并且可能提供接近或与手工实现相当的性能。其中之一是来自MS的STM.NET的DevLabs项目。
如果您不是.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调度并不是新生事物。Solaris中有LWP很久了,它们已被废弃。NT中有纤程,它们现在大多数是遗物。NetBSD中有"激活",它们已被废弃。Linux在N:M线程方面有自己的看法。现在它似乎已经有点死了。
不时地会出现新的竞争者:例如Intel的McRT,或最近微软的User-Mode Scheduling和ConCRT。它们最低层次的功能与N:M MPI调度器相同。在SMP系统上,Erlang或任何MPI系统都可能通过利用新的UMS极大地受益。
我猜OP的问题不是关于任何解决方案的优点和主观论据,而是如果我必须回答,那么这取决于任务:对于在许多核心的单个系统上运行的构建低级高性能基本数据结构,低锁定/无锁技术或STM在性能方面将产生最佳结果,即使在Erlang等语言中解决了上述问题,也很可能在性能方面击败MPI解决方案。对于在单个系统上运行的任何稍微复杂一些的构建,我可能会选择经典的粗粒度锁定或如果性能非常重要,一个STM。对于构建分布式系统,MPI系统可能是自然之选。请注意,也有.NET的MPI实现 (尽管它们似乎不是很活跃)。