Programming Clojure笔记之五——状态

在Clojure中,几乎任何事物都是一个值(value)。
状态(State)就是一个identity在某个时间点的值。
对于identity,在Clojure中提供了四种引用类型。

  • Refs,对共享状态以coordinated,synchronous方式进行更改
  • Atoms, 对共享状态以uncoordinated,synchronous方式进行更改
  • Agents,对共享状态以asynchronous方式进行更改
  • Vars,管理线程局部状态。

Ref和软事务内存(Software Transactional Memory)

;Clojure中大部分对象都是不可变的。如果需要可变数据,如下定义一个引用。
(def current-track (ref "Mars, the Bringer of War"))
-> #'user/current-track

;解引用
(deref current-track)
-> "Mars, the Bringer of War"
@current-track
-> "Mars, the Bringer of War"

ref-set

;给引用设置新的值(ref-set)
;由于引用可变,因而需要在dosync开启的事务内进行。
(dosync (ref-set current-track "Venus, the Bringer of Peace"))

STM的性质

;正如数据库事务一样,STM事务有三个性质。
;原子性(atomic)
;一致性(consistent),引用类型可以指明校验函数,任何一个校验失败,事务失败。
;隔离性(isolated),事务之间不可见。
;然而数据库事务还有一个特性,可持久性(durable)。由于STM是内存中事务,无法保证持久性。事务的四个性质简称为ACID,STM只提供了ACI。

;在同一个事务中发生的变更,称之为coordinated,即在外界看来这些变更同时发生。
(dosync
  (ref-set current-track "Credo")
  (ref-set current-composer "Byrd"))
-> "Byrd"

alter

;更新引用,使用alter可以接受一个变更函数,函数的参数包含引用当前值,返回值即为引用的新值。
(defrecord Message [sender text])
(def messages (ref ()))
(defn add-message [msg]
(dosync (alter messages conj msg)))

STM的工作原理:MVCC

STM使用的技术称之为Multiversion Concurrency Control

简单说,事务A在开始时获取一个代表时间戳的整数(point),以及所有涉及到的引用值的一份私有拷贝(effectively private copy)。所有的操作针对这份私有拷贝进行,在进行的过程中,如果有任何一个其他的事务企图对A所涉及的引用进行set/alter,那么A会强制重新开始。如果在dosync中抛出了异常,那么事务结束,不再重试。

有时你觉得alter这种过分的谨慎并无必要,可以使用性能更好的commute

commute

commute是alter的一个特殊变种,它允许更多的并发。当然正如函数名所表明的,对于commute发起的变更之间的顺序是可变的,即STM可以有重排列的自由。当前事务并不会因为其他事务更改了引用而重启。理论上聊天记录的更新不应该使用commute,然而实际中STM对commute的重排发生在微妙级别,所以不会有影响。

优先选择使用alter

很多数据更新之间的顺序是不可交换的(not commutative)。例如一个产生唯一ID的计数器。

(def counter (ref 0))
(defn next-counter [] (dosync (alter counter inc)))

其他情况也尽量使用alter而不是commute,因为alter不容易出错,最大的问题只是效率可能低一点而已。

给Ref添加校验

数据库事务为了保证一致性,会进行很多完整性检查。STM也可以通过给ref添加校验函数进行类似的检查。

(def validate-message-list
(partial every? #(and (:sender %) (:text %))))
(def messages (ref () :validator validate-message-list))
;试图添加违法数据会发生异常
(add-message "not a valid message")
-> java.lang.IllegalStateException: Invalid reference state

使用Atom进行非协调的同步更新

相较于由一个事务进行协调(coordinated)ref更新,Atoms是一种更加轻量的机制。用于非协调的对单个值进行更新。

;定义一个atom
(def current-track (atom "Venus, the Bringer of Peace"))
-> #'user/current-track

;解引用atom
(deref current-track)
-> "Venus, the Bringer of Peace"
@current-track
-> "Venus, the Bringer of Peace"

;设置新值
(reset! current-track "Credo")
-> "Credo"

;使用atom指向一个map
(def current-track (atom {:title "Credo" :composer "Byrd"}))
-> #'user/current-track

;重新设置map
(reset! current-track {:title "Spem in Alium" :composer "Tallis"})
-> {:title "Spem in Alium", :composer "Tallis"}

;仅仅设置map的一个字段,使用swap!函数
(swap! current-track assoc :title "Sancte Deus")
-> {:title "Sancte Deus", :composer "Tallis"}

使用Agent进行异步更新

;定义一个Agent
(def counter (agent 0))
-> #'user/counter

;通知Agent更新数据,注意是异步的
(send counter inc)
-> #<clojure.lang.Agent@23451c74: 0>

;等待agent完成
(await & agents)
(await-for timeout-millis & agents)

Agent添加校验和错误处理

;给counter添加一个validator
(def counter (agent 0 :validator number?))
-> #'user/counter

;使用字符串更新counter,会发生异常
(send counter (fn [_] "boo"))
-> #<clojure.lang.Agent@4de8ce62: 0>

;查看counter当前值
@counter
-> java.lang.Exception: Agent has errors

;查看counter具体出错原因
(agent-errors counter)
-> (#<IllegalStateException ...>)

;清除agent错误  
(clear-agent-errors counter)
-> nil

在事务中包含Agent

;定义存储文件名的agent
(def backup-agent (agent "output/messages-backup.clj"))

;在一个事务中,首先使用commute向messages添加msg,
;然后向backup-agent发送一个备份任务。
(defn add-message-with-backup [msg]
  (dosync
    (let [snapshot (commute messages conj msg)]
      (send-off backup-agent (fn [filename] (spit filename snapshot) filename))
      snapshot)))
;send-off函数使用一个新的线程池来执行,防止阻塞其他agent的运行。不要使用send开启一个可能阻塞的agent更新。

统一的更新模型

如上所述,refs、atoms和agents都可以通过将一个函数作用于以前的状态从而更新状态。这种处理共享状态的统一模型是Clojure的关键概念之一。总结如下:

更新机制Ref的函数Atom的函数Agent的函数
函数alterswap!send-off
函数(可交换时使用)commuteN/AN/A
函数(非阻塞动作可用)N/AN/Asend
设置新值ref-setreset!N/A

使用Vars管理线程私有状态

;在def或者defn中使用dynamic元信息创建一个动态var并提供一个根绑定。
(def ^:dynamic foo 10)
-> #'user/foo

;根绑定被所有线程共享
foo
-> 10
;在另一个线程中验证
(.start (Thread. (fn [] (println foo))))
-> nil
| 10

;使用binding宏可以创建一个线程局部绑定
;在结构上binding和let相似
(binding [foo 42] foo)
-> 42

;为了说明binding和let的区别,首先创建一个简单的函数
(defn print-foo [] (println foo))
-> #'user/print-foo

;分别在let和binding中调用print-foo函数
(let [foo "let foo"] (print-foo))
| 10
(binding [foo "bound foo"] (print-foo))
| bound foo

;let对外界没有副作用,而binding有

远程作用

用于动态绑定的var称为special variable。命名一般都用前后加一个星号的方式。例如,Clojure将标准I/O流命令为*in*、*out*、*err*。

;定义一个函数,每次执行都休眠100毫秒
(defn ^:dynamic slow-double [n]
  (Thread/sleep 100)
  (* n 2))

;再定义一个函数调用上面的函数6次
(defn calls-slow-double []
  (map slow-double [1 2 1 2 1 2]))

;使用time函数测量calls-slow-double函数的执行时间
;务必使用dorun,不然map会立即返回一个惰性序列。
(time (dorun (calls-slow-double)))
| "Elapsed time: 601.418 msecs"
-> nil

;从上例看出,slow-double执行很慢,因为一遍遍做同样的计算。
;如果能将函数的执行结果缓存起来,下次遇到相同的输入直接将缓存结果返回即可,因此我们需要将slow-double函数使用memoize函数进行改装。然后动态绑定。
(defn demo-memoize []
  (time
    (dorun
      (binding [slow-double (memoize slow-double)]
        (calls-slow-double)))))

处理Java回调

许多Java API依赖于回调事件处理器。像Swing这样的GUI框架使用事件处理器来响应用户输入。像SAX这样的XML解析器需要用户实现一个回调处理器接口。

; redacted from Clojure's xml.clj to focus on dynamic variable usage
(startElement
  [uri local-name q-name #^Attributes atts]
  ; details omitted
  (set! *stack* (conj *stack* *current*))
  (set! *current* e)
  (set! *state* :element))
nil)
(endElement
  [uri local-name q-name]
  ; details omitted
  (set! *current* (push-content (peek *stack*) *current*))
  (set! *stack* (pop *stack*))
  (set! *state* :between)
nil)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值