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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容