《Haskell并行与并发编程》——第2章,第2.2节Eval monad、rpar和rseq

简介:

本节书摘来自异步社区《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的时间线


8e3a6507bd26f4398f9fb7d888b3ac9c851f4757

从图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的时间线


739979a5372b819261bd7d1836d8cf8a2615de76

图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的时间线


3e68b6a1c8c36ade1b3401e95deadff0642822b4

对于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模式。——译者注

相关文章
|
2月前
|
Python
解释Python中的并发编程和并行编程之间的区别。
解释Python中的并发编程和并行编程之间的区别。
|
2月前
|
Rust
Rust中的Fn、FnMut 和 FnOnce都有什么区别?
在 Rust 中,`Fn`、`FnMut` 和 `FnOnce` 是三个用于表示闭包类型的 trait。闭包是一种可以捕获其环境变量的函数。在创建闭包是会默认实现这几个 trait 中的一个。
|
4月前
|
Python
Python 的异步编程:什么是协程(Coroutine)和生成器(Generator)之间的区别?
Python 的异步编程:什么是协程(Coroutine)和生成器(Generator)之间的区别?
|
4月前
|
存储 Rust 并行计算
rust高级 异步编程 一 future(1)
rust高级 异步编程 一 future
53 0
|
4月前
|
Rust 调度
rust高级 异步编程 一 future(2)
rust高级 异步编程 一 future
37 0
Generator(生成器),入门初基,Coroutine(原生协程),登峰造极,Python3.10并发异步编程async底层实现
普遍意义上讲,生成器是一种特殊的迭代器,它可以在执行过程中暂停并在恢复执行时保留它的状态。而协程,则可以让一个函数在执行过程中暂停并在恢复执行时保留它的状态,在Python3.10中,原生协程的实现手段,就是生成器,或者说的更具体一些:协程就是一种特殊的生成器,而生成器,就是协程的入门心法。
Generator(生成器),入门初基,Coroutine(原生协程),登峰造极,Python3.10并发异步编程async底层实现
|
Scala
Scala第2章 控制结构和函数(编程题)
Scala第2章 控制结构和函数(编程题)
130 0
Scala第2章 控制结构和函数(编程题)
|
缓存 开发者 Python
3_python高阶_协程—yield实现多任务
python高阶_协程—yield实现多任务
117 0
|
存储 缓存 Python
1_python高阶_协程—迭代器
python高阶_协程—迭代器
83 0