0

我想知道 Clojure 是否有针对 ABA 问题的内置解决方案。我正在创建一个显示此问题的示例,但 Clojure 以某种方式检测到更改。这是因为 Clojure 的事务比较的是引用而不是值吗?

我的例子:

(def x (ref 42))
(def io (atom false))
(def tries (atom 0))

(def t1 (Thread. (fn [] (dosync (commute x - 42)))))
(def t2 (Thread. (fn [] (dosync 
                          (Thread/sleep 100)
                          (commute x + 42)))))
(def t3 (Thread. 
  (fn [] 
    (dosync 
      (do 
        (Thread/sleep 1000) 
        (swap! tries inc) 
        (if (= 42 @x) 
          (reset! io true)))))))

(.start t3)
(.start t1)
(.start t2)

(.join t1)
@x
(.join t2)
@x
(.join t3)
@tries
(if (= true @io) (println "The answer is " @x))

Try 计数始终为 2,因此事务 t3 一定注意到了 t1 和 t2 的 ref 变化。有人知道这种行为的原因吗?

4

2 回答 2

1

在回答手头的问题之前,我只想说,到目前为止,关于 Clojure STM 的最佳信息来源——除了源代码本身——是 Mark Volkmann 的软件事务内存文章(链接指向更新日志页面,从那里点击链接到最新版本)。它非常全面。(不要担心 2009 年的时间戳,STM 没有太大变化。)如果你想弄清楚在这种情况下事情是如何工作的,我强烈建议你阅读它。


至于眼前的场景:

对于 Ref 的事务中读取,STM 承诺返回在当前事务尝试之前提交的值。(当然,除非当前事务尝试自己设置了相关 Ref 的事务中值。)该值可能是也可能不是写入 Ref 的最新值,但如果不是,则读取需要是对 Ref 的历史感到满意。如果 Ref 的历史不包含这样的值,那么一个错误记录 Ref 和事务重试。随后,由于故障,Ref 的历史链的长度可能会增加,直到 Ref 的最大历史长度(默认为 10),但请注意,这只会在有机会(另一次写入 Ref)时发生,并且只会有助于“足够晚”开始的交易(因此它们的时间戳晚于历史记录中记录的某些值)。

在手头的情况下,t3到了读取 Ref 的时间,t1并且t2将毫无问题地完成写入,x并且x将不再能够满足要求从 beforet3的第一次尝试中获得值的读取请求。(这是因为 Ref 的历史链默认从长度 0 开始,这意味着不保留任何历史值。)因此t3必须记录故障x并重试。

(如果您针对相同的 Ref 和辅助 Atom 重新运行三个事务——例如,将除前三行之外的所有内容再次粘贴到您的 REPL 中——您将在第二次运行时看到tries跳转到4,然后5在第三次运行时跳转到,表明那时有历史价值可用。)


关于ABA问题:

ABA 问题与 STM 无关,因为在适当的 ABA 场景中,“B”被写入内存位置(1)由不同的线程和(2)在“main”第一次读取“A”之后线程(意味着遭受 ABA 问题的线程),然后类似地,第二个“A”由(1)不同的线程写入,(2)在“B”写入之后,并且两个“As”都被观察到主线程,但“B”不是——但如上所述,在 STM 事务中,您无法在事务尝试开始观察到由不同线程写入 Ref 的值,因此如果您观察到第一个“A”,您将无法观察到“B”或第二个“A”。

这并不意味着 STM 不会出现与并发相关的问题——它很容易遇到写入偏差(在Wikipedia 关于快照隔离的文章中进行了描述——这是该ensure函数旨在修复的问题,但这取决于用户代码在适当的地方调用它),commute可能会被误用 &c。

于 2017-05-11T22:42:55.230 回答
0

您是正确的,这是预期的行为(尽管我本来希望tries是 1)。除了许多讨论软件事务内存 (STM) 的 Clojure 书籍外,您还可能希望查看

此外,通常最好使用alter而不是commute,这很容易出错,通常是“过早优化”的情况。

于 2017-05-10T15:09:53.107 回答