Haskell中的forkIO和协同程序

时间:2014-12-26 10:47:23

标签: multithreading haskell concurrency coroutine

我正在尝试理解Coroutines但是由于forkIO存在线程,所以我不太了解它们的用途。什么用例确实需要在线程上使用协同程序?

2 个答案:

答案 0 :(得分:7)

如果您正在讨论特定的Haskell协程实现(如果是,请添加链接)或关于一般概念,那么您的问题有点不清楚。

使用forkIO和某种线程间通信是实现协同程序的一种方法。优点是这样你可以利用多个CPU /核心,但在我看来,有几个缺点:

  • 基于显式并发IO,因此所有计算都必须在IO monad中运行。
  • 您必须明确实现线程间通信。
  • 你必须处理启动线程,更重要的是处理它们以及防止饥饿/死锁。
  • 架构(显然)是多线程的。在某些情况下,这可能是一个缺点。例如,您可能希望计算是纯粹的,确定性的,单线程的,但仍然使用协同程序的概念。

我会进一步假设你的问题是this Coroutine实施。

让我举一个小例子。假设我们想要计算大因子,但由于计算可能需要很长时间,我们希望它在每个周期后暂停,以便我们可以向用户提供一些反馈。此外,我们想要发出剩余计算周期的信号:

import Control.Monad
import Control.Monad.Coroutine
import Control.Monad.Coroutine.SuspensionFunctors
import Control.Parallel
import Data.Functor.Identity

-- A helper function, a monadic version of 'pogoStick':

-- | Runs a suspendable 'Coroutine' to its completion.
pogoStickM :: Monad m => (s (Coroutine s m x) -> m (Coroutine s m x))
                      -> Coroutine s m x -> m x
pogoStickM spring c = resume c >>= either (pogoStickM spring <=< spring) return


factorial1 :: (Monad m) => Integer -> Coroutine (Yield Integer) m Integer
factorial1 = loop 1
  where
    loop r 0 = return r
    loop r n = do
                  let r' = r * n
                  r' `par` yield n
                  (r' `pseq` loop r') (n - 1)


run1 :: IO ()
run1 = pogoStickM (\(Yield i c) -> print i >> return c) (factorial1 20) >>= print

现在让我们说我们意识到在每个周期之后屈服效率太低。相反,我们希望调用者在再次暂停之​​前决定我们应该计算多少个周期。为实现这一目标,我们只需将Yield仿函数替换为Request

factorial2 :: (Monad m) => Integer
                        -> Coroutine (Request Integer Integer) m Integer
factorial2 n = loop 1 n n
  where
    loop r t 0 = return r
    loop r t n | t >= n       = r' `par` request n >>= rec
               | otherwise    = rec t
      where
        rec t' = (r' `pseq` loop r') t' (n - 1)
        r' = r * n

run2 :: IO ()
run2 = pogoStickM (\(Request i c) -> print i >> return (c (i - 5)))
                  (factorial2 30)
       >>= print

虽然我们的run...示例基于IO,但是阶乘的计算是纯粹的,它们允许任何monad(包括Identity)。

仍然,使用Haskell's parallelism我们与报告代码并行运行纯计算(在从协程生成之前,我们创建了一个使用par来计算乘法步骤的火花。)

也许最重要的是,这些类型可以确保协程不会出现行为异常。协同程序无法解决死锁问题 - 产生或请求反馈总是与适当的响应相结合(除非调用者决定不继续使用协程,在这种情况下它会被垃圾收集器自动删除,没有阻塞的线程)

答案 1 :(得分:4)

没有用例真正地证实了协同程序。您可以使用协同程序执行的所有操作都可以使用forkIO +某些通信渠道。事实上,我相信Go(一种并发非常便宜的语言,比如在Haskell中)完全避开协同程序,并使用并发轻量级线程(“goroutines”)完成所有操作。

但是,有时forkIO 矫枉过正。有时你不需要并发,你只想将一个问题分解为概念上独立的指令流,这些指令流在某些明确确定的点上相互屈服。

考虑从文件中读取并写入另一个文件的任务。而不是拥有单片嵌套循环,更可重用的解决方案是用文件写入文件读取协程组成。当您稍后决定将文件打印到屏幕时,您根本不需要修改文件读取协程,您只需要以不同方式编写它。但请注意,这个问题与并发性无关,它与关注点的分离和可重用性有关。