我想我理解在Clojure交易中通勤和改变的想法之间的基本区别。
基本上改变了锁定'从事务的开始到结束的标识,以便多个事务必须按顺序执行。
Commute仅将锁应用于身份的实际值更改,以便事务中的其他操作可以在不同的时间运行,并且具有不同的世界视图。
但我对某事感到困惑。让我们定义一个带有副作用的函数和一个参与的函数:
(defn fn-with-side-effects [state]
(println "Hello!")
(inc state))
(def test-counter (ref 0))
现在,如果我们使用alter,我们会看到预期的行为:
user=> (dosync (alter test-counter fn-with-side-effects))
Hello!
1
但如果我们使用通勤:
user=> (dosync (ref-set test-counter 0))
0
user=> (dosync (commute test-counter fn-with-side-effects))
Hello!
Hello! ; hello is printed twice!
1
因此在通勤版本中,该函数显然只修改ref一次,因为最终值为1.但修改器函数的副作用执行两次。为什么会这样?
答案 0 :(得分:8)
我已经完成了一些实验,以了解commute
的工作原理。我想将我的解释分为三部分:
alter
commute
我认为Clojure for the Brave and True已经很好地解释了它:
swap!
实现"比较和设置"语义,意思是它在内部执行以下操作:
- 它读取原子的当前状态
- 然后将更新功能应用于该状态
- 接下来,它检查它在步骤1中读取的值是否与原子的当前值相同
- 如果是,那就交换!更新原子以引用步骤2的结果
- 如果不是,那就换掉吧!重试,再次通过步骤1完成整个过程。
醇>
swap!
适用于atom
,但了解它会帮助我们理解alter
和commute
,因为他们使用了类似的方法来更新ref
。
与atom
不同,ref
修改(通过alter
,commute
,ref-set
)必须包含在事务中。当事务开始(或重试)时,它将捕获包含ref
的所有内容的快照(因为alter
需要它)。仅在提交事务时才会修改ref
。
alter
在一项交易中,ref
将修改的所有alter
组成一个组。如果组中的任何一个ref
未通过更改,则将重试事务。基本上alter
执行以下操作:
ref
与事务捕获的快照进行比较。如果它们看起来不一样,请重试交易;其他ref
与快照进行比较。如果它们看起来不一样,请重试交易;其他ref
,不要让任何人修改它直到此交易试用期结束。如果失败(ref
已被锁定),请等待一段时间(例如100毫秒),然后重试该事务。 ref
更新为新状态。让我们展示顺利的改变。首先,我们将创建一个线程t1
到alter
3个计数器c1
,c2
和c3
slow-inc
。
(ns testing.core)
(def start (atom 0)) ; Record start time.
(def c1 (ref 0)) ; Counter 1
(def c2 (ref 0)) ; Counter 2
(def c3 (ref 0)) ; Counter 3
(defn milliTime
"Get current time in millisecond."
[]
(int (/ (System/nanoTime) 1000000)))
(defn lap
"Get elapse time since 'start' in millisecond."
[]
(- (milliTime) @start))
(defn slow-inc
"Slow increment, takes 1 second."
[x x-name]
(println "slow-inc beg" x-name ":" x "|" (lap) "ms")
(Thread/sleep 1000)
(println "slow-inc end" x-name ":" (inc x) "|" (lap) "ms")
(inc x))
(defn fast-inc
"Fast increment. The value it prints is incremented."
[x x-name]
(println "fast-inc " x-name ":" (inc x) "|" (lap) "ms")
(inc x))
(defn -main
[]
;; Initialize c1, c2, c3 and start.
(dosync (ref-set c1 0)
(ref-set c2 0)
(ref-set c3 0))
(reset! start (milliTime))
;; Start two new threads simultaneously.
(let [t1 (future
(dosync
(println "transaction start |" (lap) "ms")
(alter c1 slow-inc "c1")
(alter c2 slow-inc "c2")
(alter c3 slow-inc "c3")
(println "transaction end |" (lap) "ms")))
t2 (future)]
;; Dereference all of them (wait until all 2 threads finish).
@t1 @t2
;; Print final counters' values.
(println "c1 :" @c1)
(println "c2 :" @c2)
(println "c3 :" @c3)))
我们得到了这个:
transaction start | 3 ms ; 1st try
slow-inc beg c1 : 0 | 8 ms
slow-inc end c1 : 1 | 1008 ms
slow-inc beg c2 : 0 | 1009 ms
slow-inc end c2 : 1 | 2010 ms
slow-inc beg c3 : 0 | 2010 ms
slow-inc end c3 : 1 | 3011 ms
transaction end | 3012 ms
c1 : 1
c2 : 1
c3 : 1
顺利的程序。没有惊喜。
让我们看看如果ref
(让我们说c3
)在之前被修改((alter c3 ...)
会发生什么?{{1} })。我们会在c1
的更改过程中对其进行修改。将let
的{{1}}绑定编辑为:
t2
结果:
t2 (future
(Thread/sleep 900) ; Increment at 900 ms
(dosync (alter c3 fast-inc "c3")))
正如您所看到的,在1st-try - transaction start | 2 ms ; 1st try
slow-inc beg c1 : 0 | 7 ms
fast-inc c3 : 1 | 904 ms ; c3 being modified in thread t2
slow-inc end c1 : 1 | 1008 ms
slow-inc beg c2 : 0 | 1009 ms
slow-inc end c2 : 1 | 2010 ms
transaction start | 2011 ms ; 2nd try
slow-inc beg c1 : 0 | 2011 ms
slow-inc end c1 : 1 | 3012 ms
slow-inc beg c2 : 0 | 3013 ms
slow-inc end c2 : 1 | 4014 ms
slow-inc beg c3 : 1 | 4015 ms
slow-inc end c3 : 2 | 5016 ms
transaction end | 5016 ms
c1 : 1
c2 : 1
c3 : 2
的第1步中,它意识到(alter c3 ...)
(val = 1)看起来与事务捕获的快照(val = 0)不同,所以它重试交易。
现在,如果c3
(让我们说ref
)在更改<{1>}期间被修改,该怎么办?我们将修改帖子c1
上的(alter c1 ...)
。将c1
的{{1}}绑定编辑为:
t2
结果:
let
这一次,在第一次尝试的第3步 - t2
中,它发现t2 (future
(Thread/sleep 900) ; Increment at 900 ms
(dosync (alter c1 fast-inc "c1")))
已被修改,因此它要求重试事务。
现在,让我们尝试修改transaction start | 3 ms ; 1st try
slow-inc beg c1 : 0 | 8 ms
fast-inc c1 : 1 | 904 ms ; c1 being modified in thread t2
slow-inc end c1 : 1 | 1008 ms
transaction start | 1009 ms ; 2nd try
slow-inc beg c1 : 1 | 1009 ms
slow-inc end c1 : 2 | 2010 ms
slow-inc beg c2 : 0 | 2011 ms
slow-inc end c2 : 1 | 3011 ms
slow-inc beg c3 : 0 | 3012 ms
slow-inc end c3 : 1 | 4013 ms
transaction end | 4014 ms
c1 : 2
c2 : 1
c3 : 1
(让我们说(alter c1 ...)
)更改(ref
) 。我们会在ref
的更改过程中对其进行修改。
c1
结果:
(alter c1 ...)
自第一次尝试 - c2
已锁定t2 (future
(Thread/sleep 1600) ; Increment at 1600 ms
(dosync (alter c1 fast-inc "c1")))
(第4步)后,没有人可以修改transaction start | 3 ms ; 1st try
slow-inc beg c1 : 0 | 8 ms
slow-inc end c1 : 1 | 1009 ms
slow-inc beg c2 : 0 | 1010 ms
fast-inc c1 : 1 | 1604 ms ; try to modify c1 in thread t2, but failed
fast-inc c1 : 1 | 1705 ms ; keep trying...
fast-inc c1 : 1 | 1806 ms
fast-inc c1 : 1 | 1908 ms
fast-inc c1 : 1 | 2009 ms
slow-inc end c2 : 1 | 2011 ms
slow-inc beg c3 : 0 | 2012 ms
fast-inc c1 : 1 | 2110 ms ; still trying...
fast-inc c1 : 1 | 2211 ms
fast-inc c1 : 1 | 2312 ms
fast-inc c1 : 1 | 2413 ms
fast-inc c1 : 1 | 2514 ms
fast-inc c1 : 1 | 2615 ms
fast-inc c1 : 1 | 2716 ms
fast-inc c1 : 1 | 2817 ms
fast-inc c1 : 1 | 2918 ms ; and trying....
slow-inc end c3 : 1 | 3012 ms
transaction end | 3013 ms ; 1st try ended, transaction committed.
fast-inc c1 : 2 | 3014 ms ; finally c1 modified successfully
c1 : 2
c2 : 1
c3 : 1
,直到此轮交易试用结束。
那是(alter c1 ...)
的。
那么,如果我们不希望c1
,c1
,alter
所有群组在一起,该怎么办?假设我想在c1
或c2
无法更改(在交易期间被其他线程修改)时重试仅交易。我并不关心c3
的状态。如果在事务期间修改了c1
,则无需重试事务,这样我就可以节省一些时间。我们如何实现这一目标?是的,通过c3
。
c2
基本上,c2
执行以下操作:
commute
运行提供的功能(不是快照),但不对结果做任何事情。commute
。 (commute
只是我的名字。)我实际上并不知道为什么ref
必须执行第1步。在我看来,第2步就足够了。 real-commute
执行以下操作:
real-commute
,直到此交易试用期结束(如果尚未锁定),否则重试该交易。commute
创建新状态。real-commute
更新为新状态。让我们检查一下。将ref
的绑定编辑为:
ref
结果:
ref
如果在事务提交之前使用let
,t1 (future
(dosync
(println "transaction start |" (lap) "ms")
(alter c1 slow-inc "c1")
(commute c2 slow-inc "c2") ; changed to commute
(alter c3 slow-inc "c3")
(println "transaction end |" (lap) "ms")))
t2 (future)
一次,transaction start | 3 ms
slow-inc beg c1 : 0 | 7 ms ; called by alter
slow-inc end c1 : 1 | 1008 ms
slow-inc beg c2 : 0 | 1009 ms ; called by commute
slow-inc end c2 : 1 | 2009 ms
slow-inc beg c3 : 0 | 2010 ms ; called by alter
slow-inc end c3 : 1 | 3011 ms
transaction end | 3012 ms
slow-inc beg c2 : 0 | 3012 ms ; called by real-commute
slow-inc end c2 : 1 | 4012 ms
c1 : 1
c2 : 1
c3 : 1
,则slow-inc
会被调用两次。第一个commute
没有对commute
的结果做任何事情。
real-commute
可以被召唤两次以上。例如,让我们尝试修改线程commute
上的slow-inc
:
slow-inc
结果:
c3
在第一次交易试用中,在评估t2
后,t2 (future
(Thread/sleep 500) ; modify c3 at 500 ms
(dosync (alter c3 fast-inc "c3")))
发现transaction start | 2 ms
slow-inc beg c1 : 0 | 8 ms
fast-inc c3 : 1 | 504 ms ; c3 modified at thread t2
slow-inc end c1 : 1 | 1008 ms
slow-inc beg c2 : 0 | 1009 ms ; 1st time
slow-inc end c2 : 1 | 2010 ms
transaction start | 2012 ms
slow-inc beg c1 : 0 | 2012 ms
slow-inc end c1 : 1 | 3013 ms
slow-inc beg c2 : 0 | 3014 ms ; 2nd time
slow-inc end c2 : 1 | 4015 ms
slow-inc beg c3 : 1 | 4016 ms
slow-inc end c3 : 2 | 5016 ms
transaction end | 5017 ms
slow-inc beg c2 : 0 | 5017 ms ; 3rd time
slow-inc end c2 : 1 | 6018 ms
c1 : 1
c2 : 1
c3 : 2
与快照不同,从而触发交易重试。如果(commute c2 ...)
在(alter c3 ...)
之前,则会在评估之前触发重审或c3
。因此,在所有(alter c3 ...)
s 之后放置所有(commute c2 ...)
可能会为您节省一些时间。
如果我们在评估(commute c2 ..)
中的交易时修改线程commute
中的alter
,我们会看到会发生什么。
c2
结果:
t2
正如您所看到的,由于t1
,没有重新进行交易,t2 (future
(Thread/sleep 500) ; before evaluation of (commute c2 ...)
(dosync (alter c2 fast-inc "c2"))
(Thread/sleep 1000) ; during evaluation of (commute c2 ...)
(dosync (alter c2 fast-inc "c2"))
(Thread/sleep 1000) ; after evaluation of (commute c2 ...)
(dosync (alter c2 fast-inc "c2")))
仍然更新为我们的预期值(4)。
现在我想在transaction start | 3 ms
slow-inc beg c1 : 0 | 9 ms
fast-inc c2 : 1 | 504 ms ; before
slow-inc end c1 : 1 | 1009 ms
slow-inc beg c2 : 1 | 1010 ms
fast-inc c2 : 2 | 1506 ms ; during
slow-inc end c2 : 2 | 2011 ms
slow-inc beg c3 : 0 | 2012 ms
fast-inc c2 : 3 | 2508 ms ; after
slow-inc end c3 : 1 | 3013 ms
transaction end | 3013 ms
slow-inc beg c2 : 3 | 3014 ms
slow-inc end c2 : 4 | 4014 ms
c1 : 1
c2 : 4
c3 : 1
中演示步骤1的效果:它的c2
是读写锁定的。首先,确认它是读锁定的:
real-commute
结果:
real-commute
ref
被屏蔽,直到t2 (future
(Thread/sleep 3500) ; during real-commute
(println "try to read c2:" @c2 " |" (lap) "ms"))
解锁。这就是为什么transaction start | 3 ms
slow-inc beg c1 : 0 | 9 ms
slow-inc end c1 : 1 | 1010 ms
slow-inc beg c2 : 0 | 1010 ms
slow-inc end c2 : 1 | 2011 ms
slow-inc beg c3 : 0 | 2012 ms
slow-inc end c3 : 1 | 3012 ms
transaction end | 3013 ms
slow-inc beg c2 : 0 | 3013 ms
slow-inc end c2 : 1 | 4014 ms
try to read c2: 1 | 4015 ms ; got printed after transaction trial ended
c1 : 1
c2 : 1
c3 : 1
在4000毫秒后被评估的原因,即使我们的订单是3500毫秒的睡眠状态。
由于@c2
和c2
需要阅读他们的println
来执行给定的功能,因此他们将被阻止,直到他们的commute
也被解锁。您可以尝试将alter
替换为ref
。效果应与此示例相同。
因此,为了确认它是写锁定的,我们可以使用ref
:
(println ...)
结果:
(alter c2 fast-inc "c2")
从这里你还可以猜出ref-set
做了什么:
t2 (future
(Thread/sleep 3500) ; during real-commute
(dosync (ref-set c2 (fast-inc 9 " 8"))))
已被写锁定,请在一段时间后重试该事务(例如100 ms);否则告诉事务在执行佣金时将此transaction start | 3 ms
slow-inc beg c1 : 0 | 8 ms
slow-inc end c1 : 1 | 1008 ms
slow-inc beg c2 : 0 | 1010 ms
slow-inc end c2 : 1 | 2011 ms
slow-inc beg c3 : 0 | 2012 ms
slow-inc end c3 : 1 | 3013 ms
transaction end | 3014 ms
slow-inc beg c2 : 0 | 3014 ms
fast-inc 8 : 9 | 3504 ms ; try to ref-set but failed
fast-inc 8 : 9 | 3605 ms ; try again...
fast-inc 8 : 9 | 3706 ms
fast-inc 8 : 9 | 3807 ms
fast-inc 8 : 9 | 3908 ms
fast-inc 8 : 9 | 4009 ms
slow-inc end c2 : 1 | 4015 ms
fast-inc 8 : 9 | 4016 ms ; finally success, c2 ref-set to 9
c1 : 1
c2 : 9
c3 : 1
更新为给定值。ref-set
在步骤1中被锁定时, ref
也会失败。与ref
或real-commute
不同,它不会等待一段时间重试交易。如果ref
被锁定的时间过长,这可能会导致问题。例如,我们会尝试使用alter
修改ref-set
后的ref
:
c1
结果:
commute
回想一下,t2 (future
(Thread/sleep 2500) ; during alteration of c3
(dosync (commute c1 fast-inc "c1")))
在transaction start | 3 ms
slow-inc beg c1 : 0 | 8 ms
slow-inc end c1 : 1 | 1008 ms
slow-inc beg c2 : 0 | 1010 ms
slow-inc end c2 : 1 | 2011 ms
slow-inc beg c3 : 0 | 2012 ms
fast-inc c1 : 1 | 2506 ms
fast-inc c1 : 1 | 2506 ms
fast-inc c1 : 1 | 2506 ms
...
Exception in thread "main" java.util.concurrent.ExecutionException:
java.lang.RuntimeException: Transaction failed after reaching retry
limit, compiling: ...
更改后被c1
写入锁定,因此alter
会一直失败并继续重试该事务。在没有缓冲时间的情况下,它达到了交易再审限制并且蓬勃发展。
real-commute
通过让用户减少将导致事务重试的commute
来帮助改善并发性,并且调用给定函数的成本至少两次以更新其ref
。在某些情况下,ref
可能比commute
慢。例如,当事务中唯一要做的事情是更新alter
时,ref
的费用高于commute
:
alter
结果:
(def c (ref 0)) ; counter
(defn slow-inc
[x]
(Thread/sleep 1000)
(inc x))
(defn add-2
"Create two threads to slow-inc c simultaneously with func.
func can be alter or commute."
[func]
(let [t1 (future (dosync (func c slow-inc)))
t2 (future (dosync (func c slow-inc)))]
@t1 @t2))
(defn -main
[& args]
(dosync (ref-set c 0))
(time (add-2 alter))
(dosync (ref-set c 0))
(time (add-2 commute)))
以下是"Elapsed time: 2003.239891 msecs" ; alter
"Elapsed time: 4001.073448 msecs" ; commute
:
alter
&#39; s t1
已启动。alter
&#39; s t2
已开始。alter
&#39; t1
成功,alter
已提交,t1
已成为1。c
&#39; s t2
发现alter
与其快照不同(步骤2),重试交易。c
&#39; t2
成功,alter
已提交,t2
成为2。 c
的程序:
commute
&#39; s t1
已启动。commute
&#39; s t2
已开始。commute
&#39; s t1
已启动。 real-commute
已被锁定。c
&#39; s t2
已启动。它发现real-commute
已被锁定,因此它会重试该事务(步骤1)。c
&#39; t2
已启动,但commute
已被锁定,因此已被阻止。c
&#39; s t1
已结束,已提交交易。 real-commute
变为1. c
已取消阻止。t2
&#39; s t2
已启动。real-commute
&#39; s t2
已结束,已提交交易。 real-commute
变为2。这就是为什么c
在这个例子中慢于commute
的原因。
这可能与clojuredocs.org中的example of commute相矛盾。在他的例子中,关键的区别在于交易体中发生的延迟(100毫秒),但在我的例子中,alter
发生了延迟。这种差异导致他的slow-inc
阶段运行得非常快,从而减少了锁定时间和阻塞时间。锁定时间越短意味着再审的可能性越小。这就是为什么在他的例子中,real-commute
比commute
快。将他的alter
更改为inc
,您将获得与我相同的观察结果。
这就是全部。
答案 1 :(得分:6)
我明白了。
这是因为通勤功能总是执行两次。
Commute允许比alter更多的潜在并发性,因为它不会在整个事务持续时间内锁定身份。
相反,它在事务开始时读取标识的值一次,并且当调用通勤操作时,它返回应用于此值的通勤函数。
这个值现在完全有可能已经过时了,因为其他一些线程可能在事务开始和通勤函数执行之间的某个时间发生了变化。
但是,保持完整性是因为在实际修改ref时,在提交时执行了通勤功能。
该网站对差异有一个非常明确的解释:http://squirrel.pl/blog/2010/07/13/clojure-alter-vs-commute/
事实上,当调用通勤时,它会立即返回运行结果 参考的功能。在交易的最后它执行 再次计算,这次同步(如改变)更新 裁判。这就是为什么最终计数器值为51即使是 最后一个帖子打印了45。
因此,如果您的通勤功能有副作用,请小心,因为它们会被执行两次!!