控制并行执行

12
Haskell提供了一个名为par的组合器,它会将"spark"排队,以与当前线程可能并行评估。它还提供了一个叫做pseq的组合器,它强制执行纯代码以按特定顺序进行评估。
Haskell似乎没有提供一种方法来生成多个spark,然后等待它们全部完成。这在显式并发中非常简单,但在纯spark中似乎是不可能的。
部分原因或许是因为spark的预期用例。它们似乎是为推测性评估而设计的。也就是说,做一些可能需要但也可能不需要的工作。因此,spark只在其他空闲核心上运行。
然而,这并非我的用例。我有一堆结果,确信很快就会需要它们。如果我开始尝试处理结果之前先启动spark,那么我最终只会单线程运行而且spark也会被烧掉。
当然,如果par 等待 Sparks完成,它就无法实现任何并行性!但如果有一种方法可以产生多个 spark,然后等待它们全部完成,那将非常有用。但是我找不到任何实现方式。
有人有什么有用的建议吗?(除了“显式并发”,显然。)

3
你可以使用 Par monad,它虽然不是显式地支持并发,但可以让你显式地等待结果。 - kosmikus
1
+1 给 Par monad(顺便说一下,来自 monad-par),它提供了更多关于并行性的具体控制。 - Louis Wasserman
非常简短的答案:不用担心等待Spark。使用-feager-blackholing编译,可能会没问题。 - John L
2个回答

6
你可以尝试将计算结果放入严格的数据结构中。
{-# LANGUAGE BangPatterns #-}
module Main where

import Control.Parallel

fib :: Int -> Int
fib n
    | n < 1     = 0
    | n == 1    = 1
    | otherwise = fib (n-1) + fib (n-2)

trib :: Int -> Int
trib n
    | n < 1     = 0
    | n < 3     = 1
    | otherwise = trib (n-1) + trib (n-2) + trib (n-3)

data R = R { res1, res2 :: !Int }

main :: IO ()
main = do
    let !r = let a = fib 38
                 b = trib 31
             in a `par` b `pseq` (R a b)
    print $ res1 r
    print $ fib 28
    print $ res2 r

这在这里起作用:

$ ./spark +RTS -N2 -s
39088169
317811
53798080
          65,328 bytes allocated in the heap
           9,688 bytes copied during GC
           5,488 bytes maximum residency (1 sample(s))
          30,680 bytes maximum slop
               2 MB total memory in use (0 MB lost due to fragmentation)

                                    Tot time (elapsed)  Avg pause  Max pause
  Gen  0         1 colls,     0 par    0.00s    0.00s     0.0001s    0.0001s
  Gen  1         1 colls,     1 par    0.00s    0.00s     0.0001s    0.0001s

  Parallel GC work balance: 1.33 (686 / 515, ideal 2)

                        MUT time (elapsed)       GC time  (elapsed)
  Task  0 (worker) :    0.59s    (  0.59s)       0.00s    (  0.00s)
  Task  1 (worker) :    0.00s    (  0.59s)       0.00s    (  0.00s)
  Task  2 (bound)  :    0.59s    (  0.59s)       0.00s    (  0.00s)
  Task  3 (worker) :    0.00s    (  0.59s)       0.00s    (  0.00s)

  SPARKS: 1 (1 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)

  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time    1.17s  (  0.59s elapsed)
  GC      time    0.00s  (  0.00s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time    1.18s  (  0.59s elapsed)

  Alloc rate    55,464 bytes per MUT second

  Productivity  99.9% of total user, 199.1% of total elapsed

gc_alloc_block_sync: 0
whitehole_spin: 0
gen[0].sync: 0
gen[1].sync: 0

这对我来说在使用惰性数据结构时也有效。我认为这只是用于par/pseq的预期语义。 - John L
嗯,看起来严格绑定大多数情况下已经足够了。如果任务所需时间显著不同,我偶尔会遇到 R 具有严格或惰性字段的失败火花,但不足以说明严格字段是否有所不同。字段严格性背后的想法是强制所有火花在继续之前都已完成,而惰性字段则不会强制执行此操作。 - Daniel Fischer
我认为最好不要强制评估字段,因为这样每个Spark在使用值之前会有更多的时间完成,这将使Spark熄灭的可能性更小。但是由于使用站点非常接近返回的值,所以在这个例子中可能没有任何区别。 - John L

6

非常简短的回答

你不能这样做。

简短的回答

要等待火花完成,可以尝试评估火花正在评估的内容。例如,如果您有表达式 ab,要计算 a + b,可以进行如下操作:

a `par` b `par` (a + b)

或者

a `par` b `pseq` (a + b)
长回答 当使用 par 来创建一个火花时,您告诉运行时系统:“我稍后会需要这个值,所以你应该并行地评估它。” 稍后如果需要该值,则火花已经评估了表达式或者没有。如果已经评估,则惰性求值将被替换为值,因此重新评估不会产生任何成本 - 它只是获取该值。如果火花没有评估该表达式,则等待火花无意义 - 等待可能需要一段时间才能被安排,并且等待的线程浪费时间。相反,您应该自己评估表达式。基本上,没有必要等待火花。您只需尝试评估原始表达式并获得性能优势。
另外,关于推测的注意事项 - 虽然火花可以而且经常用于推测,但这并不完全是它们的设计目的。我更经常看到人们像下面的 pfib 中那样将 par 用于简单的并行化,而不是用于推测。 示例 标准的例子是将斐波那契数列并行化,从串行实现开始。
fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)

到并行

pfib 0 = 0
pfib 1 = 1
pfib n = l `par` r `pseq` (l + r) where
    l = pfib (n - 1)
    r = pfib (n - 2)

现在举个使用预测的例子:

spec :: a -- a guess to the input value
    -> (a -> b) -- a function to tranform the input value
    -> a -- the actual input value - this will require computation
    -> b -- the function applied to the input value
spec guess f input = let speculation = f guess in speculation `par`
    if guess == input
        then speculation
        else f input

我从这个speculation的Hackage软件包中得到了这个函数,实际上有一些优化,比如不在单核上执行此操作以及检查输入是否已经被计算过,但这并不影响函数的工作。

其他更明确的解决方案


我唯一要补充的是,你应该使用编译时选项-feager-blackholing,它会影响如何标记thunk正在被评估。 - John L
据我所知,Par单子仅仅是一个基于IO的调度器,用于自动化处理一些事情。特别地,每次对runPar的调用都会启动整个调度器的新实例,然后等待其运行。 - MathematicalOrchid

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