使用单子的并行策略

6

我经常看到有关Haskell的并行策略在纯计算(例如fib)中的用法和解释。然而,我很少看到它与单子结构一起使用:当应用于ST sIO时,par和相关函数的效果是否有合理的解释?这样使用会得到任何加速吗?


你希望按照特定的顺序执行IO操作(例如,先打开文件,然后读取它,最后关闭它)。那么你想要并行化什么呢? - helium
1
@helium:这似乎主要出现在可变数据或使用FFI时。 - John L
我一直在思考这个问题。我经常会打开几个大文件(100兆)并使用并行线程进行解压缩,然后在它们打开后进行操作。它们足够大以获得良好的性能提升,但又足够小以便于将它们保存在内存中。我一直在想如何在Haskell中实现这个功能。 - Tim Perry
@Tim:当然可以,你只需要forkIO多个线程来完成它。 - Peaker
是时候学习forkIO了,我想…… - Tim Perry
2个回答

12

在IO单子中,并行更准确地称为“并发”,可以通过Control.Concurrent模块中的forkIO等实现。

并行化ST单子的难点在于ST必须是单线程的,这就是其目的。有一种惰性变体的ST单子,Control.Monad.ST.Lazy,原则上可以支持并行评估,但我不知道是否有人尝试过。

有一种新的用于并行评估的单子叫做Eval,可以在parallel package的最新版本中找到。我建议使用rparrseq代替parpseq来使用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)

1

有些情况下这样做是有意义的,但一般情况下你不应该这样做。请看以下内容:

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的评估,另一个线程将放弃其迄今为止所做的工作。为了确保安全,需要满足以下几个条件:

  1. 执行多次IO操作是安全的。
  2. 仅执行一些IO操作是安全的(例如,没有清理)
  3. IO操作不会出现竞争条件。如果一个线程在评估a时改变了一些数据,那么另一个也在工作中的线程是否会表现得明智?可能不会。
  4. 任何外部调用都是可重入的(当然,这对于并发性是必需的)

如果你的someIOCalc看起来像这样

someIOCalc n = do
  prelaunchMissiles
  threadDelay n
  launchMissiles

使用parunsafePerformIO绝对不安全。

现在,它是否值得呢?也许吧。火花很便宜,甚至比线程还便宜,所以理论上应该会提高性能。但实际上可能并不是这样。Roman Leschinsky有一篇关于此的不错的博客文章

就我个人而言,我发现用forkIO更容易理解。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接