繁体   English   中英

无锁多线程适合真正的线程专家

[英]Lock-free multi-threading is for real threading experts

我正在阅读Jon Skeet对一个问题的回答,他在其中提到了这一点:

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

这不是我第一次听到这个,但我发现如果你对学习如何编写无锁多线程代码感兴趣,很少有人谈论你实际上是如何做的。

所以我的问题是除了学习所有关于线程等的知识之外,你还从哪里开始尝试学习专门编写无锁多线程代码以及有哪些好的资源。

干杯

当前的“无锁”实现大部分时间都遵循相同的模式:

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

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

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

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

大多数时候的诀窍是你没有专用的锁——相反,你将例如数组中的所有元素或链表中的所有节点视为“自旋锁”。 如果自上次阅读以来没有更新,您可以阅读、修改并尝试更新。 如果有,请重试。
这使您的“锁定”(哦,抱歉,非锁定 :) 非常细粒度,而不会引入额外的内存或资源要求。
使其更细粒度会降低等待的可能性。 在不引入额外资源需求的情况下使其尽可能细粒度听起来很棒,不是吗?

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

现在,可以肯定的是,代码序列不会“自上而下”流动,而是像根本没有序列一样运行 - 并且可以称为“魔鬼的操场”。 我认为对于将发生什么加载/存储重新排序给出确切的答案是不可行的。 相反,人们总是说在玉米方面,并不妨易拉罐做最坏的打算。 “哦,CPU可能会将该读取重新排序到该写入之前,所以最好在此处放置一个内存屏障,在这个位置。”

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


要正确使用“无锁”多线程,您必须了解内存模型。
然而,获得正确的内存模型和保证MFENCE ,正如这个故事所证明的那样,英特尔和 AMD 对MFENCE的文档进行了一些更正,这在 JVM 开发人员中引起了一些骚动 事实证明,开发人员从一开始就依赖的文档并不那么精确。

.NET 中的锁会导致隐式内存屏障,因此您可以安全地使用它们(大多数情况下,也就是说……参见Joe Duffy - Brad Abrams - Vance Morrison在延迟初始化、锁、易失性和内存方面的伟大之处)障碍。:)(请务必点击该页面上的链接。)

作为额外奖励,您将在支线任务中了解 .NET 内存模型 :)

还有来自 Vance Morrison 的“oldie but goldie”: What Every Dev Must Know About Multithreaded Apps

...当然,正如@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 线程有自己的看法。 它现在似乎有些死了。
不时有新的竞争者出现:例如来自英特尔的 McRT ,或者最近的User-Mode Scheduling与来自 Microsoft 的ConCRT
在最低级别,它们执行 N:M MPI 调度程序所做的工作。 Erlang - 或任何 MPI 系统 - 可能会通过利用新的UMS使 SMP 系统受益匪浅。

我想 OP 的问题不是关于任何解决方案的优点和主观论据,但如果我必须回答这个问题,我想这取决于任务:用于构建运行在具有多核的单个系统,无论是低锁定/“无锁定”技术还是 STM 都将在性能方面产生最佳结果,并且在任何时候都可能在性能方面击败 MPI 解决方案,即使上述问题得到解决例如在 Erlang 中。
为了构建在单个系统上运行的任何稍微复杂的东西,我可能会选择经典的粗粒度锁定,或者如果性能非常重要,则选择 STM。
对于构建分布式系统,MPI 系统可能是一个自然的选择。
请注意, .NET也有MPI 实现(尽管它们似乎不那么活跃)。

乔·达菲的书:

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

他还写了一篇关于这些主题的博客。

正确使用低锁程序的诀窍是在深层次上准确理解内存模型的规则在您的硬件、操作系统和运行时环境的特定组合上是什么。

我个人还不够聪明,无法在 InterlockedIncrement 之外进行正确的低锁编程,但如果你是,那就去吧。 只要确保您在代码中留下大量文档,这样那些不如您聪明的人就不会意外破坏您的内存模型不变量之一并引入一个无法找到的错误。

现在没有“无锁线程”这样的东西。 对于学术界等来说,这是一个有趣的游乐场,早在上世纪末,当时计算机硬件又慢又贵。 Dekker 的算法一直是我最喜欢的,现代硬件已经把它放牧了。 它不再起作用了。

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

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

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

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

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

谷歌用于无锁数据结构软件事务内存

我同意约翰斯基特的观点; 无锁线程是魔鬼的游乐场,最好留给知道自己需要知道什么的人。

尽管在 .NET 中实现无锁线程可能很困难,但通常您可以通过研究需要锁定的内容并最小化锁定部分来在使用锁时做出重大改进……这也称为最小化锁粒度

例如,只需说您需要使集合线程安全。 如果迭代集合的方法对每个项目执行一些 CPU 密集型任务,请不要盲目地锁定它。 可能只需要锁定创建集合的浅表副本。 迭代副本然后可以在没有锁的情况下工作。 当然,这在很大程度上取决于您的代码的具体情况,但我已经能够使用这种方法修复锁车队问题。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM