我经常看到有关Haskell的并行策略在纯计算(例如fib
)中的用法和解释。然而,我很少看到它与单子结构一起使用:当应用于ST s
或IO
时,par
和相关函数的效果是否有合理的解释?这样使用会得到任何加速吗?
我经常看到有关Haskell的并行策略在纯计算(例如fib
)中的用法和解释。然而,我很少看到它与单子结构一起使用:当应用于ST s
或IO
时,par
和相关函数的效果是否有合理的解释?这样使用会得到任何加速吗?
在IO单子中,并行更准确地称为“并发”,可以通过Control.Concurrent
模块中的forkIO
等实现。
并行化ST单子的难点在于ST必须是单线程的,这就是其目的。有一种惰性变体的ST单子,Control.Monad.ST.Lazy
,原则上可以支持并行评估,但我不知道是否有人尝试过。
有一种新的用于并行评估的单子叫做Eval,可以在parallel package的最新版本中找到。我建议使用rpar
和rseq
代替par
和pseq
来使用Eval
单子,因为这样可以编写更加健壮和易读的代码。例如,通常的fib
示例可以写成:
fib n = if n < 2 then 1 else
runEval $ do
x <- rpar (fib (n-1))
y <- rseq (fib (n-2))
return (x+y)
有些情况下这样做是有意义的,但一般情况下你不应该这样做。请看以下内容:
doPar =
let a = unsafePerformIO $ someIOCalc 1
b = unsafePerformIO $ someIOCalc 2
in a `par` b `pseq` a+b
doPar
中,会触发对a
的计算,然后主线程会评估b
。但是,在主线程完成计算b
之后,它也可能会开始评估a
。现在你有两个线程在评估a
,这意味着某些IO操作将执行两次(或可能更多)。但是,如果一个线程完成了对a
的评估,另一个线程将放弃其迄今为止所做的工作。为了确保安全,需要满足以下几个条件:
a
时改变了一些数据,那么另一个也在工作中的线程是否会表现得明智?可能不会。如果你的someIOCalc
看起来像这样
someIOCalc n = do
prelaunchMissiles
threadDelay n
launchMissiles
使用par
和unsafePerformIO
绝对不安全。
现在,它是否值得呢?也许吧。火花很便宜,甚至比线程还便宜,所以理论上应该会提高性能。但实际上可能并不是这样。Roman Leschinsky有一篇关于此的不错的博客文章。
就我个人而言,我发现用forkIO
更容易理解。