并行Haskell。限制生产者的速度

时间:2014-07-16 05:51:58

标签: haskell parallel-processing monads

在Haskell的并行和并发编程中,Simon Marlow根据以下数据提供了Stream a,以及一些生产者和消费者:

data IList a
  = Nil
  | Cons a (IVar (IList a))

type Stream a = IVar (IList a)

streamFromList :: NFData a => [a] -> Par (Stream a)
streamFromList xs = do
      var <- new
      fork $ loop xs var
      return var
    where
      loop [] var = put var Nil
      loop (x:xs) var = do
        tail <- new
        put var (Cons x tail)
        loop xs tail

Later,他提到了这种方法的缺点并提出了一个解决方案:

  

在我们之前的示例中,消费者比生产者更快。相反,如果生产者比消费者更快,那么就没有什么可以阻止生产者在消费者面前走很长的路并在内存中建立一个长的IList链。这是不可取的,因为大型堆数据结构由于垃圾收集而产生开销,因此我们可能希望对生产者进行速率限制以避免它过早地进行。有一个技巧可以为流API添加一些自动速率限制。它需要在IList类型中添加另一个构造函数:

data IList a
    = Nil
    | Cons a (IVar (IList a))
    | Fork (Par ()) (IList a)

然而,他并没有完成这种方法:

  

我将把这个想法的其余部分作为练习让你自己尝试。看看您是否可以修改streamFromListstreamFoldstreamMap以合并Fork构造函数。块大小和fork距离应该是生成器的参数(streamFromListstreamMap)。

同样的问题has been asked on the mailing list,但没人给出答案。

那怎么能限制生产者的比率呢?

2 个答案:

答案 0 :(得分:6)

重要的部分在于loop功能:

  loop [] var = put var Nil
  loop (x:xs) var = do
    tail <- new
    put var (Cons x tail)
    loop xs tail

我们需要将fork距离f和块大小c添加为参数:

  loop _ _ [] var = put var Nil
  loop 0 c (x:xs) var = -- see below
  loop f c (x:xs) var = do
    tail <- new
    put var (Cons x tail)
    loop (f-1) c xs tail

每次迭代都会减少叉距。当前叉距离为零时我们需要做什么?我们提供Fork op t,其中op继续生成列表:

  loop 0 c (x:xs) var = do
    tail <- new
    let op = loop c xs tail
    put var (Fork op (Cons x tail))

请注意,如果列表为空,我们不会使用Fork。这是可能的,但有点傻,毕竟,没有任何东西可以留下来。现在更改streamFromList很简单:

streamFromList :: NFData a => Int -> Int -> [a] -> Par (Stream a)
streamFromList f c xs = do
  var <- new                            
  fork $ loop f c xs var                 
  return var 

现在,为了使用它,我们需要更改case中的streamFold

streamFold :: (a -> b -> a) -> a -> Stream b -> Par a
streamFold fn acc instrm = acc `seq` do
  ilst <- get instrm
  case ilst of
    Cons h t          -> streamFold fn (fn acc h) t
    Fork p (Cons h t) -> -- see below
    _                 -> return acc

请注意,我们Fork中的streamFromList中不允许使用空列表,但以防万一我们通过通配符匹配它(和Nil)。

如果遇到数据Fork,我们需要做什么?首先,我们需要使用fork来运行Par ()操作以传播t,然后我们就可以开始使用它了。所以我们的最后一个案例是

    Fork p (Cons h t) -> fork p >> streamFold fn (fn acc h) t

streamMap是类似的。只有在这种情况下,您才能再次在循环中使用其他参数,例如streamFromList

答案 1 :(得分:0)

我认为以下是有效的实现方式。

{-# LANGUAGE BangPatterns #-}

import Control.Monad.Par (IVar, Par, fork, get, new, put, put_, runPar)
import Control.DeepSeq   (NFData, rnf)

data IList a
  = Nil
  | Cons a (IVar (IList a))
  | Fork (Par ()) (IVar (IList a))

instance NFData a => NFData (IList a) where
  rnf Nil = ()
  rnf (Cons a b) = rnf a `seq` rnf b
  rnf (Fork a b) = rnf (runPar a) `seq` rnf b

type Stream a = IVar (IList a)

main :: IO ()
main = print $ sum (pipeline [1 .. 10000])

pipeline :: [Int] -> [Int]
pipeline list = runPar $ do
  strm <- streamFromList list 100 200
  xs   <- streamFold (\x y -> (y : x)) [] strm
  return (reverse xs)

streamFromList :: NFData a => [a] -> Int -> Int -> Par (Stream a)
streamFromList xs k n = do
    var <- new
    fork $ loop xs var k
    return var
  where
    loop [] var _ = put var Nil
    loop xs var 0 = do
      var' <- new
      put_ var (Fork (loop xs var' n) var')
    loop (x:xs) var i = do
      tail <- new
      put var (Cons x tail)
      loop xs tail (i - 1)

streamFold :: (a -> b -> a) -> a -> Stream b -> Par a
streamFold fn !acc strm = do
  ilst <- get strm
  case ilst of
    Nil      -> return acc
    Cons h t -> streamFold fn (fn acc h) t
    Fork p s -> fork p >> streamFold fn acc s

在这里,streamFromList(生产者)的值流中,而streamFold并行地使用它们。在第一个k值之后,streamFromListFork放入流中。 Fork包含产生下一个n值的计算,以及可以从中使用这些值的流。

在这一点上,如果消费者落后于生产者,则有机会追赶。到达Fork时,fork是所包含的生产者。生产者和使用者都可以再次并行进行,直到生产者在另一个n值之后,向流中添加另一个Fork并重复该循环。