请将您的想法分享到 2024年Clojure状态调查!

欢迎!请参阅关于页面以了解更多关于此服务的信息。

+1
引用、代理、原子
可能存在两个线程在同一个原子上调用 `reset!` 时相互交错的结果,导致对应的观察器调用时使用相同的老值和不同的新值。这违背了原子更新原子性的保证。


(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]]。这将“序列化”重置操作,并为观察器提供更准确的信息。这与使用 (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. 预期行为的替代方案

可能原子和 reset! 并不保证在观察器调用方面进行序列化更新。在这种情况下,最好在 atom 的文档字符串中注意这一点。

h2. 分析

Atom.reset 的代码以非原子方式读取和设置内部 AtomicReference。这允许多个线程交错地进行读取和设置,导致在通知观察器时保持过时的值。请注意,这不会影响新值,只是旧值。

h2. 针对方法

在 Atom.reset() 中,应首先进行验证,然后运行一个循环调用内部状态的 compareAndSet(类似于在 swap() 中的实现),直到 compareAndSet 返回 true 为止。请注意,这仍然比上面显示的 swap! 常态模式更快,因为它只验证一次,而且更紧密的循环应该有更少的交错。但它具有相同的观察器行为。


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,它会使用带有 reset! 和 swap! 的代码不断运行,其中验证器休眠 10ms。在两种情况下,它都会打印出唯一值的数量(应该等于 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

添加 atom-reset-atomic-watch-2015-06-30.patch。包括测试和实现。

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