6.5 使用Vars管理线程本地状态
当你调用def或defn创建一个动态变量时,只是创建了一个变量。
当你传一个初始值给def时,该初始值就成了def创建变量的根绑定(root binding),如:
(def foo 10)
变量foo的根绑定被所有线程共享,验证如下:
当前线程中
user=> foo
10
新起的线程中
user=> (.start (Thread. (fn [] (println foo))))
user=> 10
你也可以通过binding宏为变量创建一个线程本地(thread-local)绑定:
(binding [bindings] & body)
绑定有动态作业域,换句话说,就是一个绑定在执行它的线程中从绑定的地方开始直到退出线程为止都可见,但对其他线程不可见。
举例说明:
(binding [foo 42] foo)
为了说明binding和let的不同创建一个简单的输出foo值的函数
user=> (defn print-foo [] (println foo))
#'user/print-foo
然后分别通过let和binding来调用print-foo
user=> (let [foo "let foo"] (print-foo))
10
user=> (binding [foo "bound foo"] (print-foo))
bound foo
从执行结果可以看出,let在let形式外就不起作用,因此还是输出10;而binding则从binding形式开始的任何向下调用链中都有效,因此输出"bound foo"。
远距离操作
动态绑定的变量有时被称为特殊变量,它们的名字以星号(*)开始和结尾,如标准输入输出流*in*,*out*和*err*。动态绑定可以实现远距离操作,当你改变动态绑定时,你在没有改变任何函数参数的情况下改变了远处函数的行为。
有一种远距离操作临时增强了函数的行为,在某些语言中被归为面向切面编程,但在Clojure中,只是动态绑定的一个副作用而已。
举例说明:
定义一个翻倍函数
user=>(defn slow-double [n]
(Thread/sleep 100)
(* n 2))
#'user/slow-double
user=> (defn calls-slow-double []
(map slow-double [1 2 1 2 1 2]))
#'user/calls-slow-double
统计计算用时
user=> (time (dorun (calls-slow-double)))
"Elapsed time: 600.298606 msecs"
下面对slow-double函数进行缓存
(defn demo-memoize []
(time
(dorun
(binding [slow-double (memoize slow-double)]
(calls-slow-double)))))
然后执行demo-memoize函数
user=> (demo-memoize)
"Elapsed time: 201.821212 msecs"
这个例子说明了远距离操作的强大功能和危险,通过动态绑定改变了calls-slow-double函数的行为。
与Java回调API一起工作
一些Java API依赖与回调事件处理,如GUI框架和XML解析器SAX。这些回调处理都是用可变对象写的,并且倾向于单线程。在Clojure中,实现回到处理的最好折中方式是动态绑定,引入类似变量的可变引用,又因为它们在但线程环境使用,因此不会出现并发问题。
最后总结以下Clojure的并发模型,如下:
模型 作用 函数类型
Refs和STM 并发同步更新 纯函数
Atoms 非并发同步更新 纯函数
Agents 非并发异步更新 任意
Vars 线程本地动态作业域 任意
Java锁 并发同步更新 任意
当你调用def或defn创建一个动态变量时,只是创建了一个变量。
当你传一个初始值给def时,该初始值就成了def创建变量的根绑定(root binding),如:
(def foo 10)
变量foo的根绑定被所有线程共享,验证如下:
当前线程中
user=> foo
10
新起的线程中
user=> (.start (Thread. (fn [] (println foo))))
user=> 10
你也可以通过binding宏为变量创建一个线程本地(thread-local)绑定:
(binding [bindings] & body)
绑定有动态作业域,换句话说,就是一个绑定在执行它的线程中从绑定的地方开始直到退出线程为止都可见,但对其他线程不可见。
举例说明:
(binding [foo 42] foo)
为了说明binding和let的不同创建一个简单的输出foo值的函数
user=> (defn print-foo [] (println foo))
#'user/print-foo
然后分别通过let和binding来调用print-foo
user=> (let [foo "let foo"] (print-foo))
10
user=> (binding [foo "bound foo"] (print-foo))
bound foo
从执行结果可以看出,let在let形式外就不起作用,因此还是输出10;而binding则从binding形式开始的任何向下调用链中都有效,因此输出"bound foo"。
远距离操作
动态绑定的变量有时被称为特殊变量,它们的名字以星号(*)开始和结尾,如标准输入输出流*in*,*out*和*err*。动态绑定可以实现远距离操作,当你改变动态绑定时,你在没有改变任何函数参数的情况下改变了远处函数的行为。
有一种远距离操作临时增强了函数的行为,在某些语言中被归为面向切面编程,但在Clojure中,只是动态绑定的一个副作用而已。
举例说明:
定义一个翻倍函数
user=>(defn slow-double [n]
(Thread/sleep 100)
(* n 2))
#'user/slow-double
user=> (defn calls-slow-double []
(map slow-double [1 2 1 2 1 2]))
#'user/calls-slow-double
统计计算用时
user=> (time (dorun (calls-slow-double)))
"Elapsed time: 600.298606 msecs"
下面对slow-double函数进行缓存
(defn demo-memoize []
(time
(dorun
(binding [slow-double (memoize slow-double)]
(calls-slow-double)))))
然后执行demo-memoize函数
user=> (demo-memoize)
"Elapsed time: 201.821212 msecs"
这个例子说明了远距离操作的强大功能和危险,通过动态绑定改变了calls-slow-double函数的行为。
与Java回调API一起工作
一些Java API依赖与回调事件处理,如GUI框架和XML解析器SAX。这些回调处理都是用可变对象写的,并且倾向于单线程。在Clojure中,实现回到处理的最好折中方式是动态绑定,引入类似变量的可变引用,又因为它们在但线程环境使用,因此不会出现并发问题。
最后总结以下Clojure的并发模型,如下:
模型 作用 函数类型
Refs和STM 并发同步更新 纯函数
Atoms 非并发同步更新 纯函数
Agents 非并发异步更新 任意
Vars 线程本地动态作业域 任意
Java锁 并发同步更新 任意