Haskell:并发数据结构指南

时间:2012-04-19 01:56:53

标签: performance haskell concurrency ioref

我一直在尝试理解并发性,并且我一直在尝试找出更好的方法,一个大的IORef锁或许多TVar。我已经达到了以下指导原则,我们将不胜感激,评论这些是否大致正确,或者我是否错过了这一点。


让我们假设我们的并发数据结构是一个地图m,访问方式如m[i]。让我们说我们有两个函数,f_easyf_hardf_easy很快,f_hard需要很长时间。我们假设f_easy/f_hard的参数是m的元素。

(1)如果您的交易看起来与此m[f_easy(...)] = f_hard(...)大致相同,请使用IORefatomicModifyIORef。懒惰将确保m仅在短时间内锁定,因为它已更新为thunk。计算索引有效地锁定了结构(因为某些内容会更新,但我们还不知道是什么),但是一旦知道该元素是什么,整个结构上的thunk就会移动到thunk上,而不是特定元素,然后只有那个特定元素被“锁定”。

(2)如果您的交易看起来与此m[f_hard(...)] = f_easy(...)大致相同,并且不要过多冲突,请使用大量TVar s。在这种情况下使用IORef将有效地使应用程序成为单线程,因为您无法同时计算两个索引(因为在整个结构中将存在未解决的thunk)。 TVar允许您同时计算两个索引,但是,否定的是,如果两个并发事务都访问同一个元素,其中一个是写入,则必须废弃一个事务,这会浪费时间(本来可以在其他地方使用过)。如果这种情况发生了很多,你可能会更好地使用来自IORef的锁(来自黑洞),但是如果它没有发生很多,你将会更好地与TVar进行并行。< / p>

基本上在情况(2)中,使用IORef,你可以获得100%的效率(没有浪费的工作),但只能使用1.1个线程,但如果你的冲突数量很少,可以使用TVar获得80%的效率,但使用10个线程,所以即使浪费了工作,你仍然可以快7倍。

2 个答案:

答案 0 :(得分:5)

您的指南有点类似于[1](第6节)的结果,其中分析了Haskell STM的性能:

  

“特别是,对于在事务中不执行大量工作的程序,提交开销似乎非常高。为了进一步观察这种开销,需要对提交时间过程的性能进行分析。细粒度STM锁定机制。“

当我需要的所有同步都是简单锁定将确保的同步时,我使用atomicModifyIORefMVar。在查看对数据结构的并发访问时,还取决于如何实现此数据结构。例如,如果您将数据存储在IORef Data.Map内并经常执行读/写访问,那么我认为atmoicModifyIORef会降级为单线程性能,正如您所猜测的那样,但同样的情况也是如此。一个TVar Data.Map。我的观点是,使用适合并发编程的数据结构很重要(平衡树不是)。

那就是说,在我看来,使用STM的获胜论点是可组合性:您可以将多个操作组合成单个事务而不会头疼。通常,在不引入新锁的情况下使用IORefMVar是不可能的。

[1]软件事务内存(STM)的限制:在多核环境中剖析Haskell STM应用程序。 http://dx.doi.org/10.1145/1366230.1366241

回答@克林顿的评论:

如果 IORef包含所有您的数据,您只需使用atomicModifyIORef进行撰写即可。但是,如果您需要处理大量对该数据的并行读/写请求,则性能损失可能会变得很严重,因为对该数据的每个并行读/写请求对可能会导致冲突。

我尝试的方法是使用数据结构,其中条目本身存储在TVar内(相对于将整个数据结构放入单个TVar)。这应该减少活锁的可能性,因为交易不会经常发生冲突。

当然,您仍然希望尽可能减少交易,并且只有在绝对必要时才能保证一致性。到目前为止,我还没有遇到过将多个插入/查找操作组合到单个事务中的情况。

答案 1 :(得分:1)

除了性能之外,我还发现使用TVar的更基本原因 - 类型系统可确保您不会执行任何“不安全”操作,例如readIORefwriteIORef。共享数据是属性的属性,而不是实现的属性。编辑:unsafePerformIO始终不安全。如果您还使用readIORefatomicModifyIORef只是不安全。至少将你的IORef包装成一个新类型,只展示一个包裹的atomicModifyIORef

除此之外,请勿使用IORef,使用MVarTVar

  1. 您描述的第一种使用模式可能没有很好的性能特征。您可能最终(几乎)完全是单线程 - 由于懒惰,每次更新共享状态时都不会发生实际工作,但是无论何时需要使用此共享状态,都需要强制执行整堆累积的thunk,并且具有线性数据依赖结构。
  2. 具有80%的效率但实质上更高的并行性允许您利用越来越多的内核。未来几年,单线程代码可以实现最低性能改进。
  3. 许多单词CAS很可能以“硬件事务存储器”的形式出现在您附近的处理器中,从而使STM变得更加高效。
  4. 您的代码将更加模块化 - 如果您在设计具有单个引用后面的所有共享状态时添加更多共享状态,则必须更改每段代码。 TVars MVars在较小程度上{{1}}支持自然模块化。