2024年Clojure调查!中分享您的想法。

欢迎!请查看关于页面以了解更多关于这是如何工作的信息。

+1
Refs, agents, atoms
当两个线程在同一个atom上调用`reset!`时,它们可能会相互交织,导致相应的watch以相同的旧值和不同的新值被调用。这与原子原子更新保证相矛盾。


(defn reset-test []
  (let [my-atom (atom :start
                      :validator (fn [x] (Thread/sleep 100) true))
        watch-results (atom [])]
    (add-watch my-atom :watcher (fn [k a o n] (swap! watch-results conj [o n])))
  
    (future (reset! my-atom :next))
    (future (reset! my-atom :next))
    (Thread/sleep 500)
    @watch-results))

(reset-test)


产生[[:start :next] [:start :next]]。混合reset!和swap!时,也可以观察到类似的行为。

h2. 预期行为

在原子情况下,(reset-test) 应产生[[:start :next] [:next :next]]。这将“序列化”重置,并向watch提供更准确的信息。这与使用 (swap! my-atom (constantly :next)) 得到的行为相同。


(defn swap-test []
  (let [my-atom (atom :start
                      :validator (fn [x] (Thread/sleep 100) true))
        watch-results (atom [])]
    (add-watch my-atom :watcher (fn [k a o n] (swap! watch-results conj [o n])))
  
    (future (swap! my-atom (constantly :next)))
    (future (swap! my-atom (constantly :next)))
    (Thread/sleep 500)
    @watch-results))

(swap-test)


产生[[:start :next] [:next :next]]。最少惊讶原则表明,这两个函数应该产生类似输出。
 
h3. 预期替代行为

可能 atoms 和 reset! 不保证相对于 watch 调用进行序列化更新。在这种情况下,最好在 atom 的文档字符串中注明这一点。

h2. 分析

Atom.reset() 的代码以非原子方式读取和设置内部 AtomicReference。这允许多个线程交织获取和设置,导致在通知 watchs 时持有一个陈旧值。请注意,这不应影响新值,而只是旧值。

h2. 终端

在 Atom.reset() 内部,应该先进行验证,然后运行一个循环调用内部状态的 compareAndSet(),类似于它在 swap() 中实现的方式。请注意,这仍然比上面显示的 swap! 恒定模式要快,因为它只验证一次,更紧密的循环应该有更少的交织。但它具有相同的watch行为。


public Object reset(Object newval){
    validate(newval);
    for(;;)
        {
            Object oldval = state.get();
            if(state.compareAndSet(oldval, newval))
                {
                  notifyWatches(oldval, newval);
                  return newval;
               }
        }
}

3 个答案

0

评论者:ericnormand

我进行了一个测试,以支持我上面提出的时序声明。如果您运行文件 timingtest.clj,它将以每次 10ms 睡眠的验证器,不断执行带有 reset! 和 swap! 的代码。在这种情况下,它将打印出唯一的数量(应为 reset! 的数量,在这种情况下为 1000)和时间(使用 clojure.core/time)。时间值相对于机器,不应被视为绝对的。相反,重要的是它们之间的比率。

运行方式:java -cp clojure-1.7.0-master-SNAPSHOT.jar clojure.main timingtest.clj

结果

现有实现

"执行时间:1265.228 毫秒" 使用 reset! 的唯一值:140 "执行时间:11609.686 毫秒" 使用 swap! 的唯一值:1000 "执行时间:7010.132 毫秒" 使用 swap! 和 reset! 的唯一值:628

请注意,行为不同:swap! 序列化了观察者,reset! 没有(唯一的 1)。

建议的实现

"执行时间:1268.778 毫秒" 使用 reset! 的唯一值:1000 "执行时间:11716.678 毫秒" 使用 swap! 的唯一值:1000 "执行时间:7015.994 毫秒" 使用 swap! 和 reset! 的唯一值:1000

进行相同测试。这次,它们都序列化了观察者。时间没有显著变化。

0

评论者:ericnormand

添加了原子复位-原子观察者-2015-06-30.patch。包括测试和实现。

0
参考:https://clojure.atlassian.net/browse/CLJ-1770(由 ericnormand 报告)
...