如果以下情况属实,则此设计可能没问题:
- 读取将比写入更普遍
- 写入之间会穿插多次读取
- (可能)写入只会影响全局数据结构的一小部分
当然,考虑到这些条件,几乎任何并发系统都可以。由于您担心活锁,我怀疑您正在处理更复杂的访问模式。在这种情况下,请继续阅读。
您的设计似乎受到以下推理链的指导:
atomicModifyIORef
非常便宜,因为它只会产生 thunk
因为atomicModifyIORef
便宜,不会引起线程争用
廉价数据访问 + 无争用 = 并发 FTW!
这是此推理中缺少的步骤:您的IORef
修改只会创建 thunk,并且您无法控制评估 thunk 的位置。如果您无法控制评估数据的位置,那么您就没有真正的并行性。
由于您尚未提出预期的数据访问模式,这是推测,但是我预计会发生的情况是您对数据的重复修改将建立一个 thunk 链。然后在某个时候,您将从数据中读取并强制进行评估,从而导致所有这些 thunk 在一个线程中按顺序进行评估。此时,您可能已经编写了单线程代码。
解决此问题的方法是确保在将数据写入 IORef 之前对数据进行评估(至少在您希望的范围内)。这就是返回参数的atomicModifyIORef
用途。
考虑这些功能,旨在修改aVar :: IORef [Int]
doubleList1 :: [Int] -> ([Int],())
doubleList1 xs = (map (*2) xs, ())
doubleList2 :: [Int] -> ([Int], [Int])
doubleList2 xs = let ys = map (*2) xs in (ys,ys)
doubleList3 :: [Int] -> ([Int], Int)
doubleList3 xs = let ys = map (*2) xs in (ys, sum ys)
当您将这些函数用作参数时,会发生以下情况:
!() <- atomicModifyIORef aVar doubleList1
- 只创建一个 thunk,不评估数据。对于从下一个阅读的任何线程来说,这是一个令人不快的惊喜aVar
!
!oList <- atomicModifyIORef aVar doubleList2
- 仅评估新列表以确定初始构造函数,即(:)
or []
。仍然没有做任何真正的工作。
!oSum <- atomicModifyIORef aVar doubleList3
- 通过评估列表的总和,这保证了计算得到充分评估。
在前两种情况下,要做的工作很少,所以atomicModifyIORef
会很快退出。 但这项工作并没有在那个线程中完成,现在你不知道它什么时候会发生。
在第三种情况下,您知道工作是在预期的线程中完成的。首先创建一个 thunk 并更新 IORef,然后线程开始计算总和并最终返回结果。但是假设其他线程在计算总和时读取数据。它可能会开始评估 thunk 本身,现在你有两个线程在做重复的工作。
简而言之,这种设计并没有解决任何问题。它可能在您的并发问题并不困难的情况下工作,但对于您一直在考虑的极端情况,您仍然会在多个线程执行重复工作的情况下燃烧循环。与 STM 不同,您无法控制重试的方式和时间。至少 STM 你可以在事务中间中止,通过 thunk 评估它完全不在你的掌控之中。