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! 可能不保证相对于观察器调用的序列化更新。在这种情况下,最好在原子的文档字符串中指出这一点。

h2. 分析

原子.reset() 的代码非原子地读取和设置内部 AtomicReference。这允许多个线程交错获取和设置,导致在通知观察者时保留失效值。注意,这应该不会影响新值,只是旧值。

h2. 方法

在 Atom.reset() 内部,应首先进行验证,然后运行一个循环调用内部状态的 compareAndSet()(类似 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 报告)
...