【问题标题】:Does alter use locks internally?alter 在内部使用锁吗?
【发布时间】:2017-12-23 05:28:18
【问题描述】:
(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 “锁定”了(更改 a inc)上的引用,并且 f2 等待 f1 “释放锁定”,然后 f2 才能成功提交更改。 底层机制是什么?

【问题讨论】:

    标签: concurrency clojure


    【解决方案1】:

    您的程序说明了 STM 的一个问题:当您有多个事务在同一个状态下工作时,这些事务本质上必须是可串行化的,即串行运行(一个接一个完整地)。

    这就是为什么长时间运行的事务确实是一件非常糟糕的事情,因为它们可能会导致所有其他在同一 refs 上工作的事务重试,即使理论上它们可以很快完成。

    commute 是为缓解此问题而提供的工具。如果知道不同事务中的某些操作会更改相同的 refs 但不会(逻辑上)相互干扰,因为它们是可交换操作,您可以使用 commute 而不是 alter 来放松序列化要求。

    是的,STM 事务在内部使用锁。基本上,您可以将(alter a inc) 视为在 ref a 上获得“写锁”,如果它已被占用,则失败并重试 - 将其视为实现细节。 (有复杂性:在压力下,允许较旧的事务突破年轻事务持有的锁;此外,STM 在内部使用超时,因此在您的程序中使用超时会使这些实现细节浮出水面。)

    【讨论】:

      【解决方案2】:

      似乎因时间而异。我尝试了您的版本并得到了相同的结果。然后我稍微改了一下,得到了不同的结果:

       (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
      

      【讨论】:

      • 我是 Clojure 的新手,您的代码中的 t/spyx 是什么?我在编译你的代码时出错
      • 将其更改为println。它来自这里:github.com/cloojure/tupelo#expression-debugging
      • 我运行你的代码,得到的结果与你的不同,我的结果显示 f2 重试了
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-02
      • 2011-07-31
      • 1970-01-01
      • 2013-12-26
      • 1970-01-01
      • 2022-01-19
      相关资源
      最近更新 更多