在Clojure中更改ref时,为什么通勤函数被调用两次?

时间:2015-04-12 18:01:51

标签: clojure

我想我理解在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.但修改器函数的副作用执行两次。为什么会这样?

2 个答案:

答案 0 :(得分:8)

我已经完成了一些实验,以了解commute的工作原理。我想将我的解释分为三部分:

  • 比较并设置语义
  • alter
  • commute

比较并设置语义

我认为Clojure for the Brave and True已经很好地解释了它:

  

swap!实现"比较和设置"语义,意思是它在内部执行以下操作:

     
      
  1. 它读取原子的当前状态
  2.   
  3. 然后将更新功能应用于该状态
  4.   
  5. 接下来,它检查它在步骤1中读取的值是否与原子的当前值相同
  6.   
  7. 如果是,那就交换!更新原子以引用步骤2的结果
  8.   
  9. 如果不是,那就换掉吧!重试,再次通过步骤1完成整个过程。
  10.   

swap!适用于atom,但了解它会帮助我们理解altercommute,因为他们使用了类似的方法来更新ref

atom不同,ref修改(通过altercommuteref-set)必须包含在事务中。当事务开始(或重试)时,它将捕获包含ref的所有内容的快照(因为alter需要它)。仅在提交事务时才会修改ref

alter

在一项交易中,ref将修改的所有alter组成一个组。如果组中的任何一个ref未通过更改,则将重试事务。基本上alter执行以下操作:

  1. 将其更改ref与事务捕获的快照进行比较。如果它们看起来不一样,请重试交易;其他
  2. 使用提供的功能从快照创建新状态
  3. 再次将ref与快照进行比较。如果它们看起来不一样,请重试交易;其他
  4. 尝试写锁定ref,不要让任何人修改它直到此交易试用期结束。如果失败(ref已被锁定),请等待一段时间(例如100毫秒),然后重试该事务。
  5. 告诉事务在执行佣金时将此ref更新为新状态。
  6. 让我们展示顺利的改变。首先,我们将创建一个线程t1alter 3个计数器c1c2c3 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 ...)的。

    那么,如果我们不希望c1c1alter所有群组在一起,该怎么办?假设我想在c1c2无法更改(在交易期间被其他线程修改)时重试交易。我并不关心c3的状态。如果在事务期间修改了c1,则无需重试事务,这样我就可以节省一些时间。我们如何实现这一目标?是的,通过c3

    c2

    基本上,c2执行以下操作:

    1. 直接使用commute运行提供的功能(不是快照),但不对结果做任何事情。
    2. 要求事务在事务提交之前使用相同的参数调用commute。 (commute只是我的名字。)
    3. 我实际上并不知道为什么ref必须执行第1步。在我看来,第2步就足够了。 real-commute执行以下操作:

      1. 读取并写入锁定real-commute,直到此交易试用期结束(如果尚未锁定),否则重试该交易。
      2. 使用给定的功能从commute 创建新状态
      3. 告诉事务在执行佣金时将此real-commute更新为新状态。
      4. 让我们检查一下。将ref的绑定编辑为:

        ref

        结果:

        ref

        如果在事务提交之前使用lett1 (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毫秒的睡眠状态。

        由于@c2c2需要阅读他们的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也会失败。与refreal-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

        的程序
        • 0毫秒:alter&#39; s t1已启动。
        • 1毫秒:alter&#39; s t2已开始。
        • 1000毫秒:alter&#39; t1成功,alter已提交,t1已成为1。
        • 1001毫秒:c&#39; s t2发现alter与其快照不同(步骤2),重试交易。
        • 2001 ms:c&#39; t2成功,alter已提交,t2成为2。

        c的程序:

        • 0毫秒:commute&#39; s t1已启动。
        • 1毫秒:commute&#39; s t2已开始。
        • 1000毫秒:commute&#39; s t1已启动。 real-commute已被锁定。
        • 1001毫秒:c&#39; s t2已启动。它发现real-commute已被锁定,因此它会重试该事务(步骤1)。
        • 1002毫秒:c&#39; t2已启动,但commute已被锁定,因此已被阻止。
        • 2000 ms:c&#39; s t1已结束,已提交交易。 real-commute变为1. c已取消阻止。
        • 3002 ms:t2&#39; s t2已启动。
        • 4002 ms:real-commute&#39; s t2已结束,已提交交易。 real-commute变为2。

        这就是为什么c在这个例子中慢于commute的原因。

        这可能与clojuredocs.org中的example of commute相矛盾。在他的例子中,关键的区别在于交易体中发生的延迟(100毫秒),但在我的例子中,alter发生了延迟。这种差异导致他的slow-inc阶段运行得非常快,从而减少了锁定时间和阻塞时间。锁定时间越短意味着再审的可能性越小。这就是为什么在他的例子中,real-commutecommute快。将他的alter更改为inc,您将获得与我相同的观察结果。

        这就是全部。

答案 1 :(得分:6)

我明白了。

这是因为通勤功能总是执行两次

Commute允许比alter更多的潜在并发性,因为它不会在整个事务持续时间内锁定身份。

相反,它在事务开始时读取标识的值一次,并且当调用通勤操作时,它返回应用于此值的通勤函数。

这个值现在完全有可能已经过时了,因为其他一些线程可能在事务开始和通勤函数执行之间的某个时间发生了变化。

但是,保持完整性是因为在实际修改ref时,在提交时执行了通勤功能。

该网站对差异有一个非常明确的解释:http://squirrel.pl/blog/2010/07/13/clojure-alter-vs-commute/

  

事实上,当调用通勤时,它会立即返回运行结果   参考的功能。在交易的最后它执行   再次计算,这次同步(如改变)更新   裁判。这就是为什么最终计数器值为51即使是   最后一个帖子打印了45。

因此,如果您的通勤功能有副作用,请小心,因为它们会被执行两次!!