无锁multithreading是真正的线程专家
我正在阅读Jon Skeet给出的一个问题的答案 ,并在其中提到了这个问题:
就我而言,无锁multithreading是针对真正的线程专家,我不是其中之一。
这不是我第一次听到这个消息,但是如果你有兴趣学习如何编写无锁multithreading代码,我发现很less有人谈论你是怎么做到的。
所以我的问题是除了学习所有你可以关于线程等,你开始试图学会专门写无锁multithreading代码,什么是一些很好的资源。
干杯
目前的“无锁”实现在大多数情况下遵循相同的模式:
- *阅读一些状态并复制它**
- *修改副本**
- 做一个互锁的操作
- 如果失败则重试
(*可选:取决于数据结构/algorithm)
最后一点与螺旋锁很类似。 实际上,这是一个基本的自旋链 。 🙂
我同意@nobugz的说法:在无锁multithreading中使用的互锁操作的代价是由它必须执行的caching和内存一致性任务决定的 。
然而,你所获得的“无锁”数据结构是你的“锁”是非常精细的 。 这减less了两个并发线程访问同一个“锁”(内存位置)的机会。
大部分时间的技巧是你没有专用的锁 – 而是把例如数组中的所有元素或链表中的所有节点当作“自旋锁”来处理。 您读取,修改并尝试更新,如果自上次读取以来没有更新。 如果有的话,你重试。
这使得你的“locking”(哦,抱歉,非locking:)非常细粒度,而不会引入额外的内存或资源要求。
使其更加细化降低了等待的可能性。 尽可能地做到细粒度而不引入额外的资源需求听起来不错,不是吗?
大部分的乐趣可以来自确保正确的加载/存储顺序 。
与自己的直觉相反,CPU可以自由地对内存读/写进行重新sorting – 顺便说一下,它们非常聪明:从一个单独的线程中观察这个过程是很困难的。 但是,当您开始在多个内核上执行multithreading时,您会遇到问题。 你的直觉将会崩溃:仅仅因为你的代码中有一条指令,它并不意味着它会在更早的时候发生。 CPU可以不按顺序处理指令:他们特别喜欢用内存访问来执行指令,以隐藏主内存延迟并更好地使用它们的caching。
现在,直觉认为,一系列的代码不是“自上而下”的,而是像没有序列一样运行 – 可能被称为“恶魔的游乐场”。 我相信给出一个确切的答案是什么加载/商店重新sorting将是不可能的。 相反,一个人总是说麦当劳 , jar头和jar头,并为最坏的情况做好准备。 “哦,CPU 可能会重新排列这个读取之前,所以最好在这里把内存屏障放在这个位置。
问题很复杂,即使这些mays和mights可以跨CPU架构不同。 例如, 可能会出现这样的 情况:一个架构中不会发生的事情 可能会在另一个架构中发生 。
要获得“无锁”multithreading权限,您必须了解内存模型。
然而,正如这个故事所显示的那样,获得内存模型并保证正确的处理并不是微不足道,因此Intel和AMD对MFENCE
的文档进行了一些更正,这些文档在JVM开发人员中引起了一些轰动 。 事实certificate,开发者从一开始就依赖的文档首先不是那么精确。
在.NET中的锁导致一个隐含的内存障碍,所以你可以安全地使用它们(大多数时候,这是…例如,看到这个乔·达菲 – 布拉德·艾布拉姆斯 – 万斯莫里森懒惰的初始化,locking,挥发物和内存障碍:)(请务必按照该页面上的链接。)
作为一个额外的好处,你将被引入到一个侧面的任务.NET内存模型 。 🙂
Vance Morrison还有一个“oldie goldie”: 每个开发者都必须知道multithreading应用程序 。
…当然,正如@Eric所提到的, 乔·达菲对这个问题是一个明确的解读。
一个好的STM可以尽可能地接近细粒度的locking,并且可能会提供接近或者与手工实现相当的性能。 其中之一是来自MS的DevLabs项目的STM.NET 。
如果你不是.NET的狂热分子, Doug Lea在JSR-166上做了一些很棒的工作 。
Cliff Click对散列表的使用非常有趣,它不依赖于locking条带 – 就像Java和.NET并发哈希表一样 – 并且似乎可以很好地扩展到750个CPU。
如果您不害怕冒险进入Linux领域,下面的文章提供了更多关于当前内存架构内部的知识,以及caching线共享如何破坏性能: 每个程序员应该知道什么内存 。
@Ben对MPI提出了很多评论:我真诚地同意MPI可能在某些领域发光。 与基于MPI的解决scheme相比,一个基于MPI的解决scheme可以更容易推理,实现起来更容易,而且更容易出错。 (然而,主观上 – 对于基于STM的解决scheme也是如此)。我也打赌,正如许多成功的例子所表明的那样,在例如Erlang中正确地编写一个体面的分布式应用程序更容易。
然而,MPI在运行在单一的多核系统上时,却有其自身的成本和自身的麻烦。 例如在Erlang中,围绕进程调度和消息队列的同步问题有待解决。
另外,在其核心上,MPI系统通常实施一种针对“轻量级进程”的合作N:M调度 。 例如,这意味着轻量级进程之间不可避免的上下文切换。 诚然,它不是一个“经典的上下文切换”,而是大部分是用户空间操作,并且可以做得更快 – 但是,我真诚地怀疑它可以在20-200个周期内进行互锁操作 。 即使在Intel McRT库中,用户模式的上下文切换速度也确实比较慢 。 N:M轻量级进程调度并不新鲜。 LWP在Solaris上已经有很长一段时间了。 他们被抛弃了。 NT有纤维。 他们现在大部分是遗物。 NetBSD中有“激活”。 他们被抛弃了。 Linux有自己的N:M线程主题。 现在似乎已经有些死了。
有时会有新的竞争者,例如来自英特尔的McRT ,或者最近的微软的ConCRT 用户模式调度 。
在最底层,他们做一个N:M的MPI调度器。 Erlang或任何MPI系统都可能通过利用新的UMS而在SMP系统上获益匪浅。
我猜OP的问题不是针对任何解决scheme的优点和主观的争论,但是如果我不得不回答这个问题,那么我认为这取决于任务:为了构build低级别,高性能的基础数据结构具有多个内核的 单一系统 ,无论是低locking/“无锁”技术还是STM将在性能方面产生最佳结果,并且即使在解决了上述皱纹的情况下也可能随时在性能上击败MPI解决scheme例如Erlang。
为了构build在单个系统上运行的更为复杂的任何东西,我可能会select经典的粗粒度locking,或者性能是否非常值得关注。
为了build立一个分布式系统,一个MPI系统可能是一个自然的select。
请注意,也有用于.NET的 MPI实现 (尽pipe它们似乎不是活跃的)。
乔·达菲的书:
http://www.bluebytesoftware.com/books/winconc/winconc_book_resources.html
他还写了一个关于这些话题的博客。
让低locking程序正确的窍门是深入理解内存模型的规则在硬件,操作系统和运行时环境的特定组合上的作用。
我个人没有足够的智慧去做正确的低locking编程,但是如果你是,那么很好。 只要确保在代码中留下大量文档,以便那些不像你那么聪明的人不会意外地破坏你的内存模型不变式,并引入一个不可能发现的错误。
这些天不存在“无锁线程”的事情。 在上个世纪末,电脑硬件缓慢而昂贵,这对学术界和其他类似的学校来说是一个有趣的场所。 Dekker的algorithm一直是我最喜欢的,现代硬件已经把它放到了牧场上。 它不工作了。
两个事态发展已经结束:RAM速度和CPU之间的差距越来越大。 而且芯片制造商能够将多个CPU内核放在一个芯片上。
RAM速度问题要求芯片devise人员在CPU芯片上放置一个缓冲器。 缓冲区存储代码和数据,可由CPU内核快速访问。 并且可以以更慢的速度从/向RAM读取和写入。 这个缓冲区被称为CPUcaching,大多数CPU至less有两个。 一级caching小而快,二级大而慢。 只要CPU能从第一级caching中读取数据和指令,它就会运行得很快。 高速caching未命中是非常昂贵的,如果数据不在第一个高速caching中,它会使CPUhibernate多达10个周期,如果它不在第二个高速caching中,则会使CPU多达200个周期,并且需要从中读取内存。
每个CPU内核都有自己的caching,它们存储自己的RAM“视图”。 当CPU写入数据时,写入数据被caching,然后caching到RAM中。 不可避免的是,每个核心都将有不同的RAM内容。 换句话说,一个CPU不知道另一个CPU写入的内容,直到RAM写入周期完成, CPU刷新自己的视图。
这与线程显着不兼容。 当您必须读取另一个线程写入的数据时,您总是非常关心另一个线程的状态。 为了确保这一点,你需要明确地编程一个所谓的内存屏障。 它是一个低级CPU原语,确保所有CPU高速caching处于一致状态并具有最新的RAM视图。 所有待处理的写入都必须刷新到RAM,然后需要刷新高速caching。
这在.NET中可用,Thread.MemoryBarrier()方法实现了一个。 鉴于这是locking语句所做的工作的90%(以及95%的执行时间),您完全可以避免使用.NET为您提供的工具,并尝试实现您自己的工具。
谷歌locking免费的数据结构和软件交易记忆 。
我会同意约翰·斯基特(Skeet)的观点。 无锁线程是魔鬼的游乐场,最好留给那些知道他们知道他们需要知道的人。
说到multithreading,你必须确切地知道你在做什么。 我的意思是探索在multithreading环境中工作时可能发生的所有可能情况/案例。 无锁multithreading不是一个图书馆或一个我们整合的类,它是我们在线程中获得的知识/经验。
尽pipe在.NET中使用无锁线程可能很困难,但通常在使用锁时,通过精确研究需要locking的内容以及最大限度地减lesslocking部分,可以大大改善锁…这也称为最小化锁粒度 。
作为一个例子,只是说你需要使一个线程安全。 如果在每个项目上执行一些CPU密集型任务,那么不要盲目地围绕迭代集合的方法进行locking。 您可能只需要locking创build集合的浅表副本。 迭代复制可以工作,而没有锁。 当然,这是高度依赖于你的代码的细节,但我已经能够解决这个方法锁车队的问题。