我一直在尝试了解并发性,并且一直在尝试找出更好的方法:一个大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。计算索引有效地锁定了结构(因为某些内容将要更新,但我们还不知道什么),但是一旦知道该元素是什么,整个结构上的重击将仅移动到该特定元素上的重击,然后只有该特定元素被“锁定”。
(2)如果您的事务大致类似于m[f_hard(...)] = f_easy(...)
,并且不要冲突太多,请使用很多TVar
。在这种情况下,使用IORef
可以有效地使应用程序成为单线程,因为您无法同时计算两个索引(因为整个结构上都存在无法解析的重击)。 TVar
允许您同时计算两个索引,但是,不利的是,如果两个并发事务都访问同一元素,并且其中一个是写操作,则必须废弃一个事务,这会浪费时间(这可能会浪费时间)在其他地方使用)。如果经常发生这种情况,那么使用IORef
产生的锁(通过blackholing)可能会更好,但是如果发生的次数很少,则TVar
会获得更好的并行性。
基本上在情况(2)中,使用IORef
可以获得100%的效率(没有浪费的工作),但仅使用1.1个线程,但是对于TVar
,如果冲突较少,则可能获得80%的效率但使用10个线程,所以您即使浪费了工作,最终仍然可以快7倍。
最佳答案
您的指南与[1](第6节)的发现有些相似,在此分析了Haskell 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
回答@Clinton的评论:
如果单个IORef
包含您的所有数据,则可以简单地使用atomicModifyIORef
进行合成。但是,如果您需要处理对该数据的许多并行读/写请求,则性能损失可能会变得很严重,因为对数据的每对并行读/写请求都可能导致冲突。
我将尝试的方法是使用一种数据结构,其中条目本身存储在TVar
内部(与将整个数据结构放入单个TVar
相对)。由于事务不会经常发生冲突,因此这应该减少发生活锁的可能性。
当然,您仍然希望将事务保持尽可能小,并且仅在绝对需要保证一致性的情况下才使用可组合性。到目前为止,我还没有遇到过需要将多个插入/查找操作组合到一个事务中的情况。