我想知道为什么以下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
答案 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
事务必须重新开始。