【问题标题】:Haskell: Concurrent data structure guidelinesHaskell:并发数据结构指南
【发布时间】:2012-04-30 12:40:16
【问题描述】:

我一直在尝试了解并发性,并且一直在尝试找出更好的方法,一个大的IORef 锁或多个TVars。我已经得出以下指导方针,我们将不胜感激,关于这些是否大致正确或我是否错过了重点。


假设我们的并发数据结构是一个映射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(...),并且不要冲突太多,请使用大量TVars。在这种情况下使用IORef 将有效地使应用程序成为单线程,因为您不能同时计算两个索引(因为整个结构上会有一个未解决的thunk)。 TVars 让你同时计算出两个索引,但是,不利的是,如果两个并发事务都访问同一个元素,并且其中一个是写入,则必须废弃一个事务,这会浪费时间(可以在其他地方使用)。如果这种情况经常发生,你可能会更好地使用来自IORef 的锁(通过黑洞),但如果它不会发生很多,你将获得更好的并行性TVars。

基本上在情况 (2) 中,使用 IORef 您可能会获得 100% 的效率(不会浪费工作),但只使用 1.1 个线程,但使用 TVar 如果您的冲突数量很少,您可能会获得 80% 的效率但是使用 10 个线程,因此即使浪费了工作,您最终仍能快 7 倍。

【问题讨论】:

  • 这里有问题吗?我很惊讶MVar 完全缺席。
  • 获得“安全”变量然后拥有多个线程的另一种方法是使用通道 (haskell.org/ghc/docs/latest/html/libraries/base/…) 并拥有一个监督所有值的线程,其他线程必须联系该线程才能获得访问值

标签: performance haskell concurrency ioref


【解决方案1】:

您的指导方针有点类似于 [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

回答@Clinton 的评论:

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

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

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

【讨论】:

  • Peter:我认为 TVar 的问题在于可组合性?您始终可以使用atomicModifyIORef 方法组合多个操作(如果该 IORef 包含您的所有数据)。但是,使用TVars,如果您有每秒运行一次且每次递增 X 的事务 TA1、TA2,以及读取 X,然后在写入内容之前工作 2 秒的事务 T2,则 T2 永远不会完成。您不能使用TVars 组合TA1、...和T2。您可以通过根据创建时间赋予事务优先级来防止活锁,但TVar 似乎没有这样做。
  • @Clinton:我已经为你的评论添加了答案。
【解决方案2】:

除了性能之外,我看到使用TVar 的更根本原因——类型系统确保您不会执行任何“不安全”操作,例如readIORefwriteIORef。共享您的数据是类型的属性,而不是实现的属性。编辑:unsafePerformIO 总是不安全的。 readIORef 仅在您同时使用 atomicModifyIORef 时才不安全。至少将您的 IORef 包装在一个新类型中,并且只公开一个包装的 atomicModifyIORef

除此之外,不要使用IORef,使用MVarTVar

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

【讨论】:

  • 关于“不安全”的操作,总是有不安全的操作,例如“执行不安全IO”。这只是另一个我们知道要避免的不安全操作,除非我们非常小心。
  • 你如何使用MVar?通过不断调用takeMVarputMVar?这不类似于使用IORefatomicModifyIORef 吗?
  • 1.您可以轻松地并行分叉工作,无需等待这些 thunk 被强制执行。我相信TVars反正也很懒,你不会遇到同样的问题吗?
  • 我对@9​​87654339@ 的恐惧是活锁。也就是说,所有交易的完成时间必须少于与该交易冲突之间的最大差距。如果有一个事务 T1 每秒运行一次并触及 X,另一个事务 T2 读取 X 需要 2 秒才能完成,那么 T2 将永远不会完成。类型系统不能防止这种情况。根据使用模式,代码可能会随机失败。这比不使用“readIORef”和“writeIORef”更难避免。
  • 虽然原则上活锁是一个问题,但我不确定是否有人有任何证据表明它实际上会导致您在当前 STM 中描述的各种问题。 STM 实现也可以确保一定程度的公平性。
猜你喜欢
  • 2023-04-03
  • 2015-06-22
  • 1970-01-01
  • 1970-01-01
  • 2013-12-29
  • 2011-03-04
  • 1970-01-01
  • 1970-01-01
  • 2012-02-18
相关资源
最近更新 更多