Haskell:并发数据结构指南
我一直在试图理解并发性,而且我一直在努力找出更好的方法,一个大的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)如果您的交易看起来大致如此,那么使用大量的TVar
s m[f_hard(...)] = f_easy(...)
,并且不会太多冲突。 在这种情况下使用IORef
将有效地使应用程序成为单线程,因为无法同时计算两个索引(因为在整个结构中会存在未解决的问题)。 TVar
让你同时计算出两个索引,但是,否定的是,如果两个并发事务都访问同一个元素,而其中一个是写入,那么一个事务必须被废弃,浪费时间(可能会有已在别处使用)。 如果这种情况发生了很多,你可能会更好地使用来自IORef
锁(通过黑洞),但如果它不会发生太多的话,你将会与TVar
更好地平行。
基本上在案例(2)中,使用IORef
可以获得100%的效率(没有浪费的工作),但只使用1.1个线程,但对于TVar
如果冲突数量少,则可以获得80%的效率,但使用10个线程,即使浪费了工作,结果仍然快7倍。
您的指导方针与[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
)。 这应该会减少活锁的可能性,因为交易不会经常发生冲突。
当然,您仍然希望保持尽可能小的交易量,并且只有在确保一致性的绝对必要时才使用可组合性。 到目前为止,我还没有遇到过需要将多个插入/查找操作组合到单个事务中的场景。
除了性能之外,我还看到使用TVar
的更根本原因 - 类型系统确保您不会执行任何“不安全”操作,如readIORef
或writeIORef
。 数据共享是该类型的属性,而不是实现。 编辑: unsafePerformIO
始终是不安全的。 如果您还在使用atomicModifyIORef
则readIORef
只是不安全的。 至少将你的IORef包装成新类型,并且只暴露一个包装的atomicModifyIORef
除此之外,不要使用IORef
,使用MVar
或者TVar
TVars
和在较小程度上MVars
支持自然模块化。