Clojure学习笔记(四)——状态

所谓状态,就是在某个时间点上一个标识所代表的值。

Clojure 的引用模型把标识和值清晰地区分开来。在 Clojure 中,几乎所有的东西都是值。为了加以标识,Clojure提供了四种引用类型。

  • 引用(Ref),负责协同地、同步地更改共享状态。
  • 原子(Atom),负责非协同地、同步地更改共享状态。
  • 代理(Agent),负责异步地更改共享状态。
  • 变量(Var),负责线程内的状态。

应用与软事务内存

Clojure 中的大多数对象都是不可变的。当你真的想要可变数据时,你必须明确地表示出来。比如说,你可以像下面这样创建一个可变的引用(ref),让它指向不可变对象。

(ref initial-state)

写个例子:

user=> (def s (ref "hahaha"))
#'user/s

引用包装并保护了对其内部状态的访问。要读取引用的内容,你可以调用deref。

(deref reference)

deref函数可以缩写为读取器宏@。

user=> (deref s)
"hahaha"
user=> @s
"hahaha"

ref-set

你可以使用ref-set来改变一个引用所指向的位置。

(ref-set reference new-value)

因为引用是可变的,你必须在更新它们时施以保护。在Clojure中,你可以使用事务。事务被包裹在dosync之中。

(dosync& exprs)

修改前面的引用。

user=> (dosync (ref-set s "heihei"))
"heihei"

事务的属性

和数据库事务一样,STM事务也具有一些重要的性质。

  • 更新是原子的(atomic)。如果你在一个事务中更新了多个引用,所有这些更新的累积效果,在事务外部看来,就好像是在一个瞬间发生的。
  • 更新是一致的(consistent)。可以为引用指定验证函数。如果这些函数中的任何一个失败了,整个事务都将失败。
  • 更新是隔离的(isolated)。运行中的事务,无法看到来自于其他事务的局部完成结果。

alter

Clojure的alter能在事务中对引用对象应用一个更新函数。

(alter ref update-fn & args...)

alter会返回这个引用在事务中的新值。当事务成功完成后,引用将获得它在事务中的最后一个值。用alter来替代ref-set能使代码更具可读性。

看个简单的例子。

(def messages (ref ()))

(defn add-message [msg]
  (dosync (alter messages conj msg)))

(add-message "abc")
(add-message "123")

(println @messages)

输出如下:

(123 abc)

注意这里的更新函数用了conj,alter函数调用它的update-fn时,把当前引用的值作为其第一个参数,这正是conj所期望的。

STM工作原理:MVCC

Clojure 的 STM 采用了一种名为多版本并发控制(Multiversion Concurrency Control,MVCC)的技术,这种技术也被用在了几个主要的数据库中。

下面说明了在Clojure中,MVCC是如何运作的。

事务 A 启动时会获取一个“起始点”,这个起始点就是一个简单的数字,被当作STM世界中的唯一时间戳。在事务A中访问任何一个引用,实际上访问的是这个引用与起始点相关的一份高效副本。Clojure 的持久性数据结构使得提供这些高效的私有副本相当廉价。

在事务A中,对引用进行操作时依赖(以及返回)的这个私有副本的值,被称为事务内的值。在任意时间点,如果STM检测到其他事务设置或更改了某个引用,而事务A正好也想要设置或更改,那么事务A将被迫重来。如果你在dosync块中抛出了一个异常,那么事务A会终止,而非重试。

事务A一旦提交,它一直以来那些私有的写操作就会暴露给外部世界,而且是在这个事务时间轴的一个点上瞬间发生的。

commute

commute是一种特殊的alter变体,允许更多并发。

(commute ref update-fn & args...)

当然,这需要进行权衡。之所以名为 commute ,是因为它们必须是可交换的commutative)。也就是说,更新操作必须能以任何的次序出现。这就赋予了 STM 系统对commute重新排序的自由。

使用原子进行非协同、同步的更新

相比引用,原子是一种更加轻量级的机制。在事务中对多个引用进行更新会被协同,而原子则允许更新单个的值,不与其他的任何事物协同。

你可以使用atom来创建原子,它的函数签名与ref非常类似。

(atom initial-state options?)
; options包括:
; :validator一个验证函数
; :meta一个元数据映射表

创建一个原子。

user=> (def s (atom "haha"))
#'user/s

对原子解引用就可以得到它的值,这和引用是一样的。

user=> @s
"haha"

原子并不参与事务,因而不需要dosync。要为一个原子设置值,简单的调用reset!即可。

(reset! an-atom newval)

修改上面的原子。

user=> (reset! s "ddd")
"ddd"

使用代理进行异步更新

有的应用程序会有这样一些任务,任务之间只需要很少地协同就能彼此独立进行。Clojure提供了代理来支持这种风格的任务。

代理和引用有很多共同点。和引用一样,你可以通过包装初始状态来创建代理。

(agent initial-state)

下面创建了一个计数器的代理,并把初始计数值设置为0。

user=> (def counter (agent 0))
#'user/counter

一旦得到了一个代理,你就可以向它send一个函数,来更新其状态。send把函数update-fn放进线程池里的某个线程中开始排队,等待随后执行。

(send agent update-fn & args)

向代理进行发送,和对引用进行交换非常相像。下面告诉计数器counter,准备好要自增(inc)了。

user=> (send counter inc)
#object[clojure.lang.Agent 0x4a11eb84 {:status :ready, :val 1}]

调用send不会返回代理的新值,而是返回了代理本身。

就像引用一样,你可以用deref或是@来检查代理当前的值。

user=> @counter
1

如果你希望确保代理已经完成了你发送给他的动作,你可以调用await或者await-for。

(await & agents)
(await-for timeout-millis & agents)

这两个函数会导致当前线程阻塞,直到所有发自当前线程或代理的动作全部完成。如果超过了超时时间,await-for会返回空,否则会返回一个非空值。await没有超时时间,所以一定要小心:await愿意永远等下去。

统一的更新模型

引用、原子和代理都提供了基于它们当前的状态,通过应用其他函数来更新这些状态的函数。

更新机制 引用函数 原子函数 代理函数
应用函数 alter swap! send-off
函数(交换) commute 不适用 不适用
函数(非阻塞) 不适用 不适用 send
简单设置 ref-set reset! 不适用

用变量管理线程内状态

大多数变量都甘愿保持它们的根绑定永不改变。然而,你可以借助binding宏,为一个变量创建线程内的绑定。

(binding [bindings] & body)

绑定具有动态范围。换句话说,在binding创建的这个范围内,线程执行过程中需要经过的任何地方,绑定都是可见的,直到该线程离开了该范围。同时对于其他线程而言,绑定也是不可见的。

首先需要声明一个动态变量。

user=> (def ^:dynamic foo 10)
#'user/foo

在结构上,binding与let看起来非常相像。下面为foo创建一个线程内绑定,并检查它的值。

user=> (binding [foo 2] foo)
2

你可能感兴趣的:(Clojure学习笔记(四)——状态)