在`let`里面`clojure行为错误的`确认`功能?

时间:2016-05-21 22:17:37

标签: concurrency clojure transactions

我想知道为什么以下2次调用的行为会有所不同,具体取决于ensure内部或外部引入了let函数:

=> "inside let"
(def account (ref 1000))
(def secured (ref false))
(def started (promise))
=> #'user/account
=> #'user/secured
=> #'user/started
(defn withdraw [account amount secured]
  (dosync
    (let [secured-value (ensure secured)]
      (deliver started true)
      (Thread/sleep 5000)
      (println :started)
      (when-not secured-value
        (alter account - amount))
      (println :finished))))
=> #'user/withdraw
(future (withdraw account 500 secured))
@started
(dosync (ref-set secured true))
=> #<core$future_call$reify__6320@7fbde8ed: :pending>
=> true
:started
:finished
=> true
@account
=> 500

======

    => "outside let"
(def account (ref 1000))
(def secured (ref false))
(def started (promise))
=> #'user/account
=> #'user/secured
=> #'user/started
(defn withdraw [account amount secured]
  (dosync
    (let [secured-value @secured]
      (deliver started true)
      (Thread/sleep 5000)
      (println :started)
      (when-not (ensure secured)
        (alter account - amount))
      (println :finished))))
=> #'user/withdraw
(future (withdraw account 500 secured))
@started
(dosync (ref-set secured true))
=> #<core$future_call$reify__6320@6adadff8: :pending>
=> true
=> true
:started
:started
:finished
@account
=> 1000

这里的预期语义是当secured设置为true时,人们不应该提取任何款项。

我的理解是ensure函数将确保secured ref在事务的时间跨度内没有改变,因此事务重启的第二个行为似乎是合理的,但为什么它的行为不同在第一种情况下?

更新:尝试没有Tread / sleep:

(def account (ref 1000))
(def secured (ref false))
(def started (promise))
=> #'user/account
=> #'user/secured
=> #'user/started
(defn withdraw [account amount secured]
  (dosync
    (let [secured-value (ensure secured)]
      (deliver started true)
      ;(Thread/sleep 5000)
      (println :started)
      (when-not secured-value
        (alter account - amount))
      (println :finished))))
=> #'user/withdraw
@account
=> 1000
(future (withdraw account 500 secured))
@started
(dosync (ref-set secured true))
=> #<core$future_call$reify__6320@6bce0fbf: :pending>
:started
:finished
=> true
=> true
@account
=> 500

ref-set

进行更多实验性调试
(def account (ref 1000))
(def secured (ref false))
(def started (promise))
=> #'user/account
=> #'user/secured
=> #'user/started
(defn withdraw [account amount secured]
  (dosync
    (let [secured-value (ensure secured)]
      (deliver started true)
      (Thread/sleep 5000)
      (println :started)
      (when-not secured-value
        (alter account - amount))
      (println :finished))))
=> #'user/withdraw
(future (withdraw account 500 secured))
@started
(dosync do ((println "change started") (ref-set secured true) (println "change done.")))
=> #<core$future_call$reify__6320@5b60c101: :pending>
=> true
change started
...
change started
change started
:started
:finished
change done.
NullPointerException   user/eval2176/fn--2177 (form-init3061788549693294520.clj:3)
@account
=> 500

1 个答案:

答案 0 :(得分:2)

首先,我要重述您的问题(以确保我们在同一页面上):

  

由于并发(ref-set secured true)调用,我希望withdraw事务在两种情况下都失败(并重新启动) - 但我只是在非let情况下观察到重启。为什么???

这是由于Clojure中STM的一些实现细节;具体而言,使用读/写锁保护Refs的事实。

在您的第一个示例(使用let)中,您在致电(ensure secured) 之前致电Thread/sleep 。由于ensure在目标ref上获取读锁定,这意味着在整个5秒的睡眠延迟期间,您的ref被设置为只读。由于您的并发(ref-set secured true)需要在secured上完成写锁定,因此该事务将延迟到withdraw事务完成。这就是为什么在这种情况下你没有观察到重启的原因 - STM实现中的内部锁定迫使写入事务等到读取事务完成。

相反,在您的第二个示例中,您在 之后拨打(ensure secured) ,然后拨打Thread/sleep。这意味着交易不知道它需要一个secured参考值的一致值,直到 您的5秒睡眠延迟。由于事务没有做任何事情来保护secured的值(即,它没有锁定它),这意味着任何其他事务可以在此期间自由修改secured的值。 - ensure电话前的第二个延迟。在(ensure secured)调用之后,事务会意识到它需要secured ref的一致值。在您的示例中,并发ref-set调用更改了该值,因此withdraw事务必须重新开始。