美文网首页
Clojure学习笔记(四)——状态

Clojure学习笔记(四)——状态

作者: 简单一点点 | 来源:发表于2022-11-02 20:22 被阅读0次

    所谓状态,就是在某个时间点上一个标识所代表的值。

    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
    

    相关文章

      网友评论

          本文标题:Clojure学习笔记(四)——状态

          本文链接:https://www.haomeiwen.com/subject/dcietdtx.html