我一直在尝试理解并发性,并且我一直在尝试找出更好的方法,一个大的IORef
锁或许多TVar
。我已经达到了以下指导原则,我们将不胜感激,评论这些是否大致正确,或者我是否错过了这一点。
让我们假设我们的并发数据结构是一个地图m
,访问方式如m[i]
。让我们说我们有两个函数,f_easy
和f_hard
。 f_easy
很快,f_hard
需要很长时间。我们假设f_easy/f_hard
的参数是m
的元素。
(1)如果您的交易看起来与此m[f_easy(...)] = f_hard(...)
大致相同,请使用IORef
和atomicModifyIORef
。懒惰将确保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倍。
答案 0 :(得分:5)
您的指南有点类似于[1](第6节)的结果,其中分析了Haskell STM的性能:
“特别是,对于在事务中不执行大量工作的程序,提交开销似乎非常高。为了进一步观察这种开销,需要对提交时间过程的性能进行分析。细粒度STM锁定机制。“
当我需要的所有同步都是简单锁定将确保的同步时,我使用atomicModifyIORef
或MVar
。在查看对数据结构的并发访问时,还取决于如何实现此数据结构。例如,如果您将数据存储在IORef Data.Map
内并经常执行读/写访问,那么我认为atmoicModifyIORef
会降级为单线程性能,正如您所猜测的那样,但同样的情况也是如此。一个TVar Data.Map
。我的观点是,使用适合并发编程的数据结构很重要(平衡树不是)。
那就是说,在我看来,使用STM的获胜论点是可组合性:您可以将多个操作组合成单个事务而不会头疼。通常,在不引入新锁的情况下使用IORef
或MVar
是不可能的。
[1]软件事务内存(STM)的限制:在多核环境中剖析Haskell STM应用程序。 http://dx.doi.org/10.1145/1366230.1366241
回答@克林顿的评论:
如果单 IORef
包含所有您的数据,您只需使用atomicModifyIORef
进行撰写即可。但是,如果您需要处理大量对该数据的并行读/写请求,则性能损失可能会变得很严重,因为对该数据的每个并行读/写请求对可能会导致冲突。
我尝试的方法是使用数据结构,其中条目本身存储在TVar
内(相对于将整个数据结构放入单个TVar
)。这应该减少活锁的可能性,因为交易不会经常发生冲突。
当然,您仍然希望尽可能减少交易,并且只有在绝对必要时才能保证一致性。到目前为止,我还没有遇到过将多个插入/查找操作组合到单个事务中的情况。
答案 1 :(得分:1)
除了性能之外,我还发现使用TVar
的更基本原因 - 类型系统可确保您不会执行任何“不安全”操作,例如readIORef
或writeIORef
。共享数据是属性的属性,而不是实现的属性。编辑:unsafePerformIO
始终不安全。如果您还使用readIORef
,atomicModifyIORef
只是不安全。至少将你的IORef包装成一个新类型,只展示一个包裹的atomicModifyIORef
除此之外,请勿使用IORef
,使用MVar
或TVar
TVars
MVars
在较小程度上{{1}}支持自然模块化。