本节书摘来自异步社区《Haskell并行与并发编程》一书中的第2章,第2.2节Eval monad、rpar和rseq,作者【英】Simon Marlow,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.2 Eval monad、rpar和rseq
Haskell并行与并发编程
下面介绍模块Control.Parallel.Strategies提供的用于并行编程的一些基本内容,定义如下:
data Eval a
instance Monad Eval
runEval :: Eval a -> a
rpar :: a -> Eval a
rseq :: a -> Eval a
并行性是通过Eval monad表达的,具体包括rpar和rseq两个运算。组合子rpar用于描述并行,即其参数可以并行求值;而rseq则用于强制串行求值,即对其参数求值并等待结果。两者的求值的结果都是弱首范式。rpar的参数不必是未求值的计算,即thunk,若参数是已经被求值的,则不会发生任何事情,因为没有东西需要并行计算。
Eval monad提供了runEval运算用于执行Eval计算,然后返回结果。值得注意的是,runEval是纯函数,没有副作用,无需在IO monad中使用。
为了观察rpar和rseq的效果,假设有一个函数f,以及两个被f应用的参数x和y,而且f x算得比f y慢,希望能够并行的计算f x和f y的结果。下面会使用几种不同的方法编写代码,然后研究它们间的区别。首先,对f x和f y使用rpar,然后返回一对结果,如例2-1所示。
例2-1 rpar/rpar
runEval $ do
a <- rpar (f x)
b <- rpar (f y)
return (a,b)
该代码片断执行的情况如图2-5所示。
图2-5 rpar/rpar的时间线
从图2-5中可以看到f x和f y同时开始求值,而return这句也是立刻被执行的:并不等待f x或f y完成求值。在f x和f y开始并行求值的同时,剩下的程序接着执行。
下面尝试另一种写法,将第二个rpar换成rseq。
例2-2 rpar/rseq
runEval $ do
a <- rpar (f x)
b <- rseq (f y)
return (a,b)
执行后,结果如图2-6所示。
图2-6 rpar/rseq的时间线
图2-6中f x和f y仍然并行求值,但最后的return是f y完成后才执行的。这是因为使用了rseq,该函数会在返回前等待其参数完成求值。
若添加一个额外的rseq等待f x,则会等待f x和f y都完成。
例2-3 rpar/rseq/rseq
runEval $ do
a <- rpar (f x)
b <- rseq (f y)
rseq a
return (a, b)
需要注意的是,新的rseq是应用于a,第一个rpar的结果。结果如图2-7所示。
上述代码直到f x和f y都完成求值后才返回。
对使用的模式,应如何选择?
由于程序员很少会提前知道哪个计算最耗时,因此在两个计算中任意等待一个是毫无道理的,所以rpar/rseq的模式不太有用。
图2-7 rpar/rseq/rseq的时间线
对于rpar/rpar和rpar/rseq/rseq两种模式的选择,则视具体情况而定。如果期望尽早开始更多的并行计算,而且返回值不依赖任何运算的结果,那么使用rpar/rpar是合理的。反而言之,如果所有可能的并行计算都已经开始了,或下面的代码需要用到其中一个运算的结果,那么显然应该使用rpar/rseq/rseq。
下面是最后一种写法。
例2-4 rpar/rpar/rseq/rseq
runEval $ do
a <- rpar(f x)
b <- rpar(f y)
rseq a
rseq b
return (a, b)
这段代码和rpar/rseq/rseq的行为是一样的,等待两个求值完成后再返回。虽然该写法是最长的,但和其他写法相比,显得更为对称,基于该原因,这个写法可能更加可取。
通过范例程序rpar.hs可以试验这些不同的写法,程序使用Fibonacci函数模拟并行运行的大量的计算。GHC需要-threaded参数才能支持并行,程序请按下面的方式编译:
$ ghc -O2 rpar.hs -threaded
按如下方式,可以试验rpar/rpar写法,其中+RTS -N2标志告诉GHC使用双核来运行程序(请确保机器至少是双核的):
$ ./rpar 1 +RTS -N2
time: 0.00s
(24157817,14930352)
time: 0.83s
当rpar/rseq代码片断返回1,打印第一行时间戳,第二行时间戳则在最后的计算结束后打印。正如所看到的,代码片断是立即返回的。在rpar/rseq中,第二个计算(短的那个)完成后才返回:
$ ./rpar 2 +RTS -N2
time: 0.50s
(24157817,14930352)
time: 0.82s
在rpar/rseq/rseq中,返回是在最后的:
$ ./rpar 3 +RTS -N2
time: 0.82s
(24157817,14930352)
time: 0.82s
1这里指的是包含rpar和rseq的代码片断,而非rpar/rseq模式。——译者注