Cli*_*ton 1 concurrency haskell shared-state ioref
我一直在问Haskell中关于并发性的几个问题,特别是TVar
我对Livelock的问题有所顾虑TVar
.
相反,我提出了这个解决方案.
(1)将程序中的所有共享数据包装在一个数据结构中,并将其包装在一个数据结构中IORef
.(2)只需使用即可进行任何更改atomicModifyIORef
.
我相信这可以防止死锁和活锁(而TVar只会阻止前者).此外,因为atomicModifyIORef
简单地将另一个thunk链接到一个链(这是一对指针操作),这不是一个瓶颈.对数据的所有实际操作可以并行完成,只要它们不相互依赖.Haskell运行时系统将解决这个问题.
但是我觉得这太简单了.我错过了什么"陷阱"吗?
如果满足以下条件,则此设计可能正常:
当然,考虑到这些条件,几乎任何并发系统都可以.既然你关心活锁,我怀疑你正在处理更复杂的访问模式.在这种情况下,请继续阅读.
您的设计似乎受以下推理链的指导:
atomicModifyIORef
非常便宜,因为它只是创造了thunk
因为atomicModifyIORef
价格便宜,所以不会导致线程争用
便宜的数据访问+无争用=并发FTW!
以下是此推理中缺少的步骤:您的IORef
修改仅创建thunk,并且您无法控制评估thunks的位置.如果无法控制数据的评估位置,则没有真正的并行性.
由于您尚未提供预期的数据访问模式,这是猜测,但我预计会发生的事情是您对数据的重复修改将构建一系列的thunk.然后在某些时候,您将从数据中读取并强制进行评估,从而在一个线程中按顺序评估所有这些thunks.此时,您可能已经开始编写单线程代码.
解决这个问题的方法是确保在将数据写入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)
Run Code Online (Sandbox Code Playgroud)
以下是使用这些函数作为参数时会发生的情况:
!() <- atomicModifyIORef aVar doubleList1
- 只创建一个thunk,不评估任何数据.对于从aVar
下一个读取的线程来说,这是一个令人不快的惊喜!
!oList <- atomicModifyIORef aVar doubleList2
- 仅评估新列表以确定初始构造函数,即(:)
或[]
.仍然没有做过真正的工作.
!oSum <- atomicModifyIORef aVar doubleList3
- 通过评估列表的总和,这可以保证计算得到充分评估.
在前两种情况下,完成的工作很少,因此atomicModifyIORef
很快就会退出. 但是那个工作没有在那个线程中完成,现在你不知道什么时候会发生.
在第三种情况下,您知道工作是在预期的线程中完成的.首先创建一个thunk并更新IORef,然后线程开始评估总和并最终返回结果.但是假设一些其他线程在计算总和时读取数据.它可能会开始评估thunk本身,现在你有两个线程正在进行重复工作.
简而言之,这种设计并没有解决任何问题.它很可能适用于你的并发问题并不困难的情况,但对于像你一直在考虑的极端情况,你仍然会在多个线程进行重复工作的情况下烧掉周期.与STM不同,您无法控制如何以及何时重试.至少STM你可以在交易过程中中止,通过thunk评估它完全不在你的手中.
好吧,它不会好好组成.通过单个IORef序列化所有共享内存修改意味着一次只能有一个线程能够修改共享内存,所有你真正做的就是全局锁定.是的它会起作用,但它会很慢而且远不如TVars甚至是MVars那么灵活.