并发

Clojure是一门支持并发编程的语言,它提供了很多特性让我们非常的方便进行并发程序的开发。

Future

Future可以让我们在另一个线程去执行任务,它是异步执行的,所以调用了future之后会立即返回。譬如:

  1. user=> (future (Thread/sleep 3000) (println "I'am back"))
  2. #object[clojure.core$future_call$reify__6736 0x7118d905 {:status :pending, :val nil}]
  3. user=> (println "I'am here")
  4. I'am here
  5. nil
  6. user=> I'am back

在上面的例子中,sleep会阻塞当前线程的执行,但是因为我们用了future,所以clojure将其放到了另一个线程中,然后继续执行下面的语句。

有时候,我们使用了future之后,还需要知道future的任务执行的结果,知识后就需要用defer来实现了。我们可以使用defer或者@来获取futureresult,譬如:

  1. user=> (let [result (future (println "run only once") (+ 1 1))]
  2. #_=> (println (deref result))
  3. #_=> (println @result))
  4. run only once
  5. 2
  6. 2
  7. nil

deref还可以支持timeout设置,如果超过了等待时间,就返回一个默认值。

  1. user=> (deref (future (Thread/sleep 100) 0) 10 5)
  2. 5
  3. user=> (deref (future (Thread/sleep 100) 0) 1000 5)
  4. 0

我们也可以使用realized?来判断一个future是否完成

  1. user=> (realized? (future (Thread/sleep 1000)))
  2. false
  3. user=> (let [f (future)]
  4. #_=> @f
  5. #_=> (realized? f))
  6. true

Delay

Delay可以让我们定义一个稍后执行的任务,并不需要现在立刻执行。

  1. user=> (def my-delay
  2. #_=> (delay (let [msg "hello world"]
  3. #_=> (println msg)
  4. #_=> msg)))
  5. #'user/my-delay

我们可以通过@或者force来执行delay的任务

  1. user=> @my-delay
  2. hello world
  3. "hello world"
  4. user=> (force my-delay)
  5. "hello world"

Clojure会将delay的任务结果缓存,所以第二次delay的调用我们直接获取的是缓存结果。

我们可以将delayfuture一起使用,定义一个delay操作,在future完成之后,调用delay,譬如:

  1. user=> (let [notify (delay (println "hello world"))]
  2. #_=> (future ((Thread/sleep 1000) (force notify))))
  3. #object[clojure.core$future_call$reify__6736 0x2de625f3 {:status :pending, :val nil}]
  4. user=> hello world

Promise

Promise是一个承诺,我们定义了这个promise,就预期后续会得到相应的result。我们通过deliver来将result发送给对应的promise,如下:

  1. user=> (def my-promise (promise))
  2. #'user/my-promise
  3. user=> (deliver my-promise (+ 1 1))
  4. #object[clojure.core$promise$reify__6779 0x30dc687a {:status :ready, :val 2}]
  5. user=> @my-promise
  6. 2

promise也跟delay一样,会缓存deliverresult

  1. user=> (deliver my-promise (+ 1 2))
  2. nil
  3. user=> @my-promise
  4. 2

我们也可以将promisefuture一起使用

  1. user=> (let [hello-promise (promise)]
  2. #_=> (future (println "Hello" @hello-promise))
  3. #_=> (Thread/sleep 1000)
  4. #_=> (deliver hello-promise "world"))
  5. Hello world

Atom

在并发编程里面,a = a + 1这条语句并不是安全的,在clojure里面,我们可以使用atom完成一些原子操作。如果大家熟悉c语言里面的compare and swap那一套原子操作函数,其实对Clojureatom也不会陌生了。

我们使用atom创建一个atom,然后使用@来获取这个atom当前引用的值:

  1. user=> (def n (atom 1))
  2. #'user/n
  3. user=> @n
  4. 1

如果我们需要更新该atom引用的值,我们需要通过一些原子操作来完成,譬如:

  1. user=> (swap! n inc)
  2. 2
  3. user=> @n
  4. 2
  5. user=> (reset! n 0)
  6. 0
  7. user=> @n
  8. 0

Watch

对于一些数据的状态变化,我们可以使用watch来监控,一个watch function包括4个参数,关注的key,需要watchreference,譬如atom等,以及该reference之前的state以及新的state,譬如:

  1. user=> (defn watch-n
  2. #_=> [key watched old-state new-state]
  3. #_=> (if (> new-state 1)
  4. #_=> (println "new" new-state)
  5. #_=> (println "old" old-state)))
  6. user=> (def wn (atom 1))
  7. #'user/wn
  8. user=> @wn
  9. 1
  10. user=> (add-watch wn :a watch-n)
  11. #object[clojure.lang.Atom 0x4b28dcf8 {:status :ready, :val 1}]
  12. user=> (reset! wn 1)
  13. old 1
  14. 1
  15. user=> (reset! wn 2)
  16. new 2
  17. 2

Validator

我们可以使用validator来验证某个reference状态改变是不是合法的,譬如:

  1. user=> (defn validate-n
  2. #_=> [n]
  3. #_=> (> n 1))
  4. #'user/validate-n
  5. user=> (def vn (atom 2 :validator validate-n))
  6. #'user/vn
  7. user=> (reset! vn 10)
  8. 10
  9. user=> (reset! vn 0)
  10. IllegalStateException Invalid reference state clojure.lang.ARef.validate (ARef.java:33)

在上面的例子里面,我们建立了一个validator,参数n必须大于1,否则就是非法的。

Ref

在前面,我们知道使用atom,能够原子操作某一个reference,但是如果要操作一批atom,就不行了,这时候我们就要使用ref

  1. user=> (def x (ref 0))
  2. #'user/x
  3. user=> (def y (ref 0))
  4. #'user/y

我们创建了两个ref对象xy,然后在dosync里面对其原子更新

  1. user=> (dosync
  2. #_=> (ref-set x 1)
  3. #_=> (ref-set y 2)
  4. #_=> )
  5. 2
  6. user=> [@x @y]
  7. [1 2]
  8. user=> (dosync
  9. #_=> (alter x inc)
  10. #_=> (alter y inc))
  11. user=> (dosync
  12. #_=> (commute x inc)
  13. #_=> (commute y inc))

ref-set就类似于atom里面的reset!alter就类似swap!,我们可以看到,还有一个commute也能进行reference的更新,它类似于alter,但是稍微有一点不一样。

在一个事务开始之后,如果使用alter,那么在修改这个reference的时候,alter会去看有没有新的修改,如果有,就会重试当前事务,而commute则没有这样的检查。所以如果通常为了性能考量,并且我们知道程序没有并发的副作用,那就使用commute,如果有,就老老实实使用alter了。

我们通过@来获取reference当前的值,在一个事务里面,我们通过ensure来保证获取的值一定是最新的:

  1. user=> (dosync
  2. #_=> (ref-set x (ensure y)))

Dynamic var

通常我们通过def定义一个变量之后,最好就不要更改这个var了,但是有时候,我们又需要在某些context里面去更新这个var的值,这时候,最好就使用dynamic var了。

我们通过如下方式定义一个dynamic var:

  1. user=> (def ^:dynamic *my-var* "hello")
  2. #'user/*my-var*

dynamic var必须用*包裹,在lisp里面,这个叫做earmuffs,然后我们就能够通过binding来动态改变这个var了:

  1. user=> (binding [*my-var* "world"] *my-var*)
  2. "world"
  3. user=> (println *my-var*)
  4. hello
  5. user=> (binding [*my-var* "world"] (println *my-var*)
  6. #_=> (binding [*my-var* "clojure"] (println *my-var*)) (println *my-var*))
  7. world
  8. clojure
  9. world

可以看到,binding只会影响当前的stack binding