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

相关文章
|
4月前
|
Scala 开发者
Scala中的模式匹配与高阶函数:探索强大的编程范式
【7月更文挑战第11天】Scala中的模式匹配和高阶函数是两种极其强大的特性,它们不仅提升了代码的表达力和可读性,还使得开发者能够编写出更加灵活和可重用的解决方案。通过
|
4月前
|
前端开发 Scala
Scala并发编程的react、loop方法详解
在这个例子中,`MyActor`会无限循环接收和处理消息。当收到一个字符串消息时,它会打印出"Received: "加上消息内容。如果收到其他类型的消息,它会打印"Unknown message"。
27 1
|
6月前
|
Python
Python 的异步编程:什么是协程(Coroutine)和生成器(Generator)之间的区别?
Python 的异步编程:什么是协程(Coroutine)和生成器(Generator)之间的区别?
74 0
|
Scala
Scala函数式编程实战(下)
Scala函数式编程实战(下)
324 0
Generator(生成器),入门初基,Coroutine(原生协程),登峰造极,Python3.10并发异步编程async底层实现
普遍意义上讲,生成器是一种特殊的迭代器,它可以在执行过程中暂停并在恢复执行时保留它的状态。而协程,则可以让一个函数在执行过程中暂停并在恢复执行时保留它的状态,在Python3.10中,原生协程的实现手段,就是生成器,或者说的更具体一些:协程就是一种特殊的生成器,而生成器,就是协程的入门心法。
Generator(生成器),入门初基,Coroutine(原生协程),登峰造极,Python3.10并发异步编程async底层实现
|
Scala
Scala第2章 控制结构和函数(编程题)
Scala第2章 控制结构和函数(编程题)
166 0
Scala第2章 控制结构和函数(编程题)
|
Java 编译器 Scala
SCALA程序设计 第三章:控制结构和函数
SCALA程序设计 控制结构和函数
|
算法 程序员 Scala
就是个控制结构,Scala能有什么新花样呢?
编程语言中最为基础的一个概念是控制结构,几乎任何代码都无时无刻不涉及到,其实也就无外乎3种:顺序、分支和循环。本文就来介绍Scala中控制结构,主要是分支和循环。
110 0
就是个控制结构,Scala能有什么新花样呢?
|
SQL Java 数据处理
JVM 上数据处理语言的竞争:Kotlin, Scala 和 SPL
JVM 上数据处理语言的竞争:Kotlin, Scala 和 SPL
236 0
|
Scala
Scala入门到精通——第五节 函数与闭包
本节主要内容 (一)函数字面量(值函数) (二)匿名函数 (三)函数的简化 (四)函数参数 (四)闭包 函数字面量(值函数) 函数字面量(function literal),也称值函数(function values),指的是函数可以赋值给变量。 一般函数具有如下形式: 而函数字面量具有如下形式: /* 函数字面量 function lit
5841 0