在Clojure中,几乎任何事物都是一个值(value)。
状态(State)就是一个identity在某个时间点的值。
对于identity,在Clojure中提供了四种引用类型。
;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)
;由于引用可变,因而需要在dosync开启的事务内进行。
(dosync (ref-set current-track "Venus, the Bringer of Peace"))
;正如数据库事务一样,STM事务有三个性质。
;原子性(atomic)
;一致性(consistent),引用类型可以指明校验函数,任何一个校验失败,事务失败。
;隔离性(isolated),事务之间不可见。
;然而数据库事务还有一个特性,可持久性(durable)。由于STM是内存中事务,无法保证持久性。事务的四个性质简称为ACID,STM只提供了ACI。
;在同一个事务中发生的变更,称之为coordinated,即在外界看来这些变更同时发生。
(dosync
(ref-set current-track "Credo")
(ref-set current-composer "Byrd"))
-> "Byrd"
;更新引用,使用alter可以接受一个变更函数,函数的参数包含引用当前值,返回值即为引用的新值。
(defrecord Message [sender text])
(def messages (ref ()))
(defn add-message [msg] (dosync (alter messages conj msg)))
STM使用的技术称之为Multiversion Concurrency Control
简单说,事务A在开始时获取一个代表时间戳的整数(point),以及所有涉及到的引用值的一份私有拷贝(effectively private copy)。所有的操作针对这份私有拷贝进行,在进行的过程中,如果有任何一个其他的事务企图对A所涉及的引用进行set/alter,那么A会强制重新开始。如果在dosync中抛出了异常,那么事务结束,不再重试。
有时你觉得alter
这种过分的谨慎并无必要,可以使用性能更好的commute
commute是alter的一个特殊变种,它允许更多的并发。当然正如函数名所表明的,对于commute发起的变更之间的顺序是可变的,即STM可以有重排列的自由。当前事务并不会因为其他事务更改了引用而重启。理论上聊天记录的更新不应该使用commute,然而实际中STM对commute的重排发生在微妙级别,所以不会有影响。
很多数据更新之间的顺序是不可交换的(not commutative)。例如一个产生唯一ID的计数器。
(def counter (ref 0))
(defn next-counter [] (dosync (alter counter inc)))
其他情况也尽量使用alter而不是commute,因为alter不容易出错,最大的问题只是效率可能低一点而已。
数据库事务为了保证一致性,会进行很多完整性检查。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
相较于由一个事务进行协调(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
(def counter (agent 0)) -> #'user/counter
;通知Agent更新数据,注意是异步的
(send counter inc) -> #<clojure.lang.Agent@23451c74: 0>
;等待agent完成
(await & agents)
(await-for timeout-millis & agents)
;给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
(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的函数 |
---|---|---|---|
函数 | alter | swap! | send-off |
函数(可交换时使用) | commute | N/A | N/A |
函数(非阻塞动作可用) | N/A | N/A | send |
设置新值 | ref-set | reset! | N/A |
;在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 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)