七周七并发模型 第三四章笔记
date
Jul 10, 2022
slug
七周七并发模型 第三四章笔记
status
Published
tags
读书笔记
summary
函数式编程&Clojure之道
type
Post
第三章 函数式编程
函数式编程没有可变状态,所以不会遇到由共享可变状态带来的种种问题
可变状态带来的风险:
- 隐藏的逃逸状态。使用的工具类等内部的可变状态
- 逃逸的可变状态。同步方法中返回的迭代器
Clojure知识点:
- 用map或mapcat对一个序列的每个元素进行映射
- 用序列的懒惰特性来处理较大的序列,甚至无穷序列
- 用reduce将序列化简为一个(可能比较复杂的)值
- 用fold对reduce进行并行化
- Clojure不支持尾调用消除,因此代码很少使用递归
Clojure提供了功能类似于map的pmap函数,其应用函数的过程是可以并行的,pmap在需要结果时可以并行计算,但仅生成所需要的而不是全部的结果,这个特性称为半懒惰
Clojure知识点2:
- pmap可以将映射操作并行化,构造一个半懒惰的map
- 利用partition-all可以对并行的映射操作进行批处理,以提高处理效率
- fold使用分而治之的策略,可以将化简操作并行化
- Clojure.core.reducers包提供的类似map、类似mapcat、类似filter的函数返回的并不是序列,而是化简器reducible,可以说这是化简操作的关键所在
在Java这类命令式语言中,求值顺序与源码的语句顺序紧密相关。一般来说,求值顺序与其在代码中的顺序基本一致。
函数式语言中,如何安排求值顺序来获得最终结果是相对自由的,这正是函数式代码可以轻松并行的关键所在
在纯粹的函数式语言中,函数都具有引用透明性——在调用函数的地方,都可以用函数运行的结果来替换函数的调用,而不会对程序产生副作用。
描述函数式代码执行的一种方式就是不断用函数的执行结果替换函数的调用,直至得到最终结果
运行时,从这幅图的左端出发,向右端推进数据,当某函数所依赖的数据都可用时,该函数就可以执行了,这种执行方式被称为“数据流式编程”。Clojure提供了future模型和promise模型来支持这种执行方式。
Future模型
future函数可以接受一段代码,并在一个单独的线程中执行这段代码。其返回一个future对象。可以利用deref或者简写成@对future对象进行解引用,来获取其代表的值。对future对象进行解引用将阻塞当前线程,直到其代表的值变得可用。
(def sum (future (+ 1 2 3 4 5)))
(deref sum) >> 15
@sum >> 15
(let [a (future (+ 1 2)
b (future (+ 3 4))]
(+ @a @b)) >> 10
Promise模型
类似于future对象,promise对象也是异步求值的,也通过defer或者@解引用,在求值前也会阻塞线程。创建一个promise对象后,使用promise对象的代码并不会立刻执行,而是等到用deliver为promise对象赋值后才会执行。
第四章 Clojure之道——分离标识与状态
原子变量与持久数据结构
纯粹的函数式语言完全不支持可变量(变量:编译期结束后其值可能改变的量)。所以,Clojure是不纯粹的。
在命令式语言中,变量默认都是状态易变的,代码会经常修改变量。而在不纯粹的函数式语言中,变量默认是状态不易变的,代码仅在十分必要时才修改变量。Clojure的可变量,可以在保证安全性和数据一致性的同时,处理好可变状态带来的副作用。
原子变量
使用atom函数可以创建原子变量,通过defer或者@可以获得原子变量的值,使用swap!可以更新原子变量的值,reset!可以重置原子变量的值。原子变量可以是任何类型
(def my-atom (atom 42))
(deref my-atom) >> 42
@my-atom >> 42
(swap! my-atom inc) >> 43
(reset! my-atom 0) >> 0
持久数据结构
持久是指数据结构被修改时总是保留其之前的版本,这样可以为代码提供一致的数据视角。(CopyOnWrite机制)持久数据结构被修改时看上去就像创建了一个完整的副本,其实现选择了更精巧的方法,使用了共享结构(底层类似于Golang的slice)。有共同尾端的列表可以共享结构——如果两个列表具有不同的尾端,就只能进行复制了。
标识与状态
如果一个线程引用了持久数据结构,那么其他线程对数据结构的修改对该线程就是不可见的,因此持久数据结构对并发编程的意义非比寻常,其分离了标识与状态。
命令式语言中,一个变量混合了标识与状态——一个标识只能拥有一个值。这很容易忽略一个事实:状态实际上是随时间变化的一系列值。持久数据结构将标识与状态分离开——如果获取了一个标识的当前状态,无论将来对这个标识怎样修改,获取的那个状态将不再改变。
校验器
校验器是一个函数,在原子变量的值改变生效之前被调用,如果校验器返回为true,就允许这次修改,否则就放弃这次修改。
(def non-negative (atom 0 :validator #(>= % 0)))
监视器
添加监视器时需要提供一个键值和一个监视函数。原子变量的值被改变时会调用监视器,监视器是在原子变量的值改变后才被调用,且无论swap!重试多少次,监视器只会被调用一次。注意:监视器被调用时,原子变量的值可能已经再次被改变,因此监视器必须使用参数中提供的新值,而不能通过对原子变量进行解引用来获取新值。
(add-watch a :print #(println "Changed from " %3 " to " %4))
代理和软件事务内存
代理
与原子变量类似,代理包含了对一个值的引用,可以通过deref或者@获取该值。send与swap!类似,区别是,send会在代理的值更新之前立刻返回——传给send的函数将在之后的某个时间被调用。如果多个线程同时调用send,传给send的函数将被串行调用。该函数不会进行重试,并且可以具有副作用
代理也可以使用校验器,一旦代理发生错误,就会默认进入失效状态,之后对代理数据的任何操作都会失败。agent-error可以查看一个代理是否为失效状态,使用restart-agent可以重置失效状态的代理。
代理的一个很好的实践:内存日志系统。
软件事务内存 STM
通过引用可以实现STM,通过STM可以对多个变量进行并发的一致的修改。、
通过ref声明一个引用,通过deref或@获取该值。通过ref-set设置引用的值,通过alter函数来修改引用的值,只能在一个事务中才能修改引用的值。
STM事务具有原子性、一致性和隔离性。(没有持久性是因为没有STM涉及到数据库)。通过dosync创建一个事务。如果STM运行时检测到几个并发事务的修改发生冲突,那其中的一个或者几个事务将进行重试。
Clojure支持共享可变状态的三种机制:
- 原子变量可以对单一值进行隔离的、同步的更新
- 代理可以对单一值进行隔离的、异步的更新
- 引用可以对多个值进行一致的、同步的更新
当解决一个涉及多个值需一致更新的问题时,即可以使用多个引用并通过事务来保证访问一致性,也可以将这些值整合到一个数据结构中并用一个原子变量管理这个数据结构的访问一致性。
Clojure之道的主要缺点在于不支持分布式编程。它也无法直接提供容错性,很多第三方库可以为其弥补这些缺点(Akka)