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

欢迎!请访问 关于 页面以获取更多关于此如何运作的信息。

+1
Refs、agents、atoms
可能存在两个线程同时对一个原子调用 `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);
                              返回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 毫秒" Using reset! 的唯一数量:140 "经过时间:11609.686 毫秒" 使用 swap! 的唯一数量:1000 "经过时间:7010.132 毫秒" 使用 swap! 和 reset! 的唯一数量:628

请注意,行为不同:swap! 序列化了监视器,reset! 不会(1. of 唯一)。

建议的实现

"经过时间: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](https://clojure.atlassian.net/browse/CLJ-1770)(由 ericnormand 报告)
...