(def a (ref 0))
(def b (ref 0))
(def f1 (future
(dosync
(println "f1 start")
(alter a inc)
(Thread/sleep 500)
(alter b inc)
(println "f1 end"))))
(def f2 (future
(dosync
(println "f2 start")
(alter a inc)
(println "f2 end"))))
@f1 @f2
在上面的例子中,我认为线程f2应该在f1之前终止,尽管f1在f2之前到达表达式(alter a inc),但是f1继续执行它的耗时,所以f2首先提交,因此,委员会f1,它发现ref a已经被修改,然后f1应该重试。 但结果显示我错了,它打印出以下内容:
f1 start
f2 start
f2 start
f2 start
f2 start
f2 start
f1 end
f2 start
f2 end
它的f2重试了,似乎f1"锁定"引用(改变一个公司),并且f2等待f1到"释放锁定"在f2成功实施改变之前。 什么是潜在的机制?
答案 0 :(得分:0)
您的程序说明了STM的一个问题:当您有多个事务处理相同的状态时,这些事务必须基本上是可序列化的,即串行运行(一个接一个地完成)。
这就是为什么长时间运行的事务确实是一件非常糟糕的事情,因为它们可能导致所有其他正在处理同一个refs的事务重试,即使理论上它们可以很快完成。
commute
是用于缓解此问题的工具。如果你知道不同事务中的某些操作改变了相同的引用,但由于它们是可交换的操作而没有(逻辑上)相互干扰,你可以使用commute
而不是{{1放宽可序列性要求。
是的,STM事务在内部使用锁。基本上,您可以将alter
视为在ref (alter a inc)
上获取'write lock'并且如果已经执行则失败并重试 - 请考虑这是一个实现细节。 (有一些复杂情况:在压力下,旧的交易被允许突破年轻交易所持有的锁;同样,STM在内部使用超时,因此在程序中使用超时会将这些实现细节带到表面。)
答案 1 :(得分:0)
似乎根据时间而变化。我尝试了你的版本并得到了相同的结果。然后我改了一下,得到了不同的结果:
(let [a (ref 0)
b (ref 0)
f1 (future
(dosync
(println "f1 start")
(alter a inc)
(Thread/sleep 500)
(alter b inc)
(println "f1 end")))
f2 (future
(dosync
(println "f2 start")
(alter a inc)
(println "f2 end")))]
(println @f1)
(println @f2)
(println @a)
(println @b))
结果:
Testing tst.demo.core
f1 start
f2 start
f2 start
f2 end
f1 start
f1 end
(clojure.core/deref f1) => nil
(clojure.core/deref f2) => nil
(clojure.core/deref a) => 2
(clojure.core/deref b) => 1
因此,在任何特定时间任何特定机器上的线程的详细时间似乎都会产生很大的不同。
我承认我很惊讶,因为我会预测和你一样。你的结果和新的结果都不是我所期望的。
<强>更新强>
我修改了一下然后得到了预期的结果:
(let [a (ref 0)
b (ref 0)
f1 (future
(dosync
(println "f1 start")
(alter a inc)
(Thread/sleep 500)
(alter b inc)
(println "f1 end")))
f2 (future
(dosync
(Thread/sleep 100)
(println "f2 start")
(alter a inc)
(println "f2 end")))]
(println @f1)
(println @f2)
(println @a)
(println @b))
f1 start
f2 start
f2 end
f1 start
f1 end
(clojure.core/deref f1) => nil
(clojure.core/deref f2) => nil
(clojure.core/deref a) => 2
(clojure.core/deref b) => 1