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

本文涉及的产品
视频直播,500GB 1个月
简介:

本节书摘来自异步社区《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模式。——译者注

相关文章
|
8月前
|
Python
解释Python中的并发编程和并行编程之间的区别。
解释Python中的并发编程和并行编程之间的区别。
55 0
|
7月前
|
Python
在Python中,`map()`, `filter()` 和 `reduce()` 是函数式编程中的三个核心高阶函数。
【6月更文挑战第24天】Python的`map()`应用函数到序列元素,返回新序列;`filter()`筛选满足条件的元素,生成新序列;`reduce()`累计操作序列元素,返回单一结果。
48 3
|
Python
Python的Lambda函数: 一把极简编程的瑞士军刀
Python的Lambda函数: 一把极简编程的瑞士军刀
88 0
|
8月前
|
大数据 Python
Python中的`yield`:掌握生成器的精髓
【4月更文挑战第17天】`yield`在Python中用于创建生成器,一种节约内存的迭代器。生成器函数在迭代时暂停并保存状态,下次迭代时继续执行,适用于处理大数据、实现协程和优化内存。`yield`不同于普通函数,不立即计算所有结果,而是在需要时生成单个值。使用场景包括生成大列表、实现协程和简化迭代逻辑。注意`yield`后的值不能是表达式,生成器只能调用一次,且`yield`与`return`作用不同。理解并善用`yield`能提升Python编程效率。
|
8月前
|
Python
Python中的Lambda函数应用及性能优化
Lambda函数是Python中一种简洁而强大的编程工具,本文将介绍Lambda函数的基本语法及在实际开发中的应用场景,同时探讨如何通过性能优化提升Lambda函数的执行效率。
|
8月前
|
存储 Python
介绍Python中的函数式编程工具,如`map`、`filter`和`reduce`。
介绍Python中的函数式编程工具,如`map`、`filter`和`reduce`。
56 3
|
8月前
|
Python
Python 的异步编程:什么是协程(Coroutine)和生成器(Generator)之间的区别?
Python 的异步编程:什么是协程(Coroutine)和生成器(Generator)之间的区别?
87 0
Generator(生成器),入门初基,Coroutine(原生协程),登峰造极,Python3.10并发异步编程async底层实现
普遍意义上讲,生成器是一种特殊的迭代器,它可以在执行过程中暂停并在恢复执行时保留它的状态。而协程,则可以让一个函数在执行过程中暂停并在恢复执行时保留它的状态,在Python3.10中,原生协程的实现手段,就是生成器,或者说的更具体一些:协程就是一种特殊的生成器,而生成器,就是协程的入门心法。
Generator(生成器),入门初基,Coroutine(原生协程),登峰造极,Python3.10并发异步编程async底层实现
|
Python
Python多层装饰器执行顺序
Python多层装饰器执行顺序
166 0
|
缓存 开发者 Python
3_python高阶_协程—yield实现多任务
python高阶_协程—yield实现多任务
170 0