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

欢迎!请查阅关于页面以获取更多关于如何使用本站的信息。

0
引用、代理、原子

嗨,

我在REPL中使用引用时遇到了一些奇怪的行为。
因此,我有一个具有简单监视器函数的引用

(defn watcher [_ r o n]
  (printf "ref: %s old: %s new: %s time: %d\n" @r o n (System/currentTimeMillis))
  (flush))

(def x (ref 2))
(add-watch x nil watcher) 

然后,我连续两次评估以下表达式

(future (dosync (ensure x) (Thread/sleep 3000) (alter x * 2)))

我预期监视器函数应该连续两次打印引用的值,大约每隔3秒。
但什么都没有发生。
当我尝试用另一个命令访问引用时,例如

(dosync (ensure x) (alter x inc))

REPL完全阻塞。一次我几分钟后确实得到了预期的结果,另一次我得到了这个错误信息

Execution error at reftest.core/eval24449 (form-init12758928819079876959.clj:34).                                               
Transaction failed after reaching retry limit

我发现令人困惑的是,当我评估这个let块时,一切看起来都很正常

(let [x (ref 2)]
  (add-watch x nil watcher)
  (println "start:" (deref x))
  (future (dosync (ensure x) (Thread/sleep 3000) (alter x * 2)))
  (future (dosync (ensure x) (Thread/sleep 3000) (alter x * 2)))
  (dosync (ensure x) (alter x inc))
  (println "end:" @x))

这种行为在REPL中是某种类型的错误,还是我在做某些错误?
我应该如何确保在真实代码中这样的STM事务不会失败?

您可以通过在`dosync`体中散布打印语句来验证Alex所说的情况。`let`变体如果在其调用`future`之间添加例如100ms的睡眠也可能需要挂起。尽管这可能更多地是出于偶然而不是规则。

1 个答案

+2
by
选定 by
 
最佳答案

我假设两个事务都在失败并多次重试,每个重试都非常耗时,直到其中一个意外地得到正确的时间来工作或达到最大重试次数。

你的事务中永远不应当执行阻塞操作。

by
此外,您不必确保在事务中是否更改了相同的引用。
by
你好 Alex,感谢你关于阻塞操作的警告。

我使用了 ensure 上面的代码,因为我发现当我不加它时,第三个事务会在前面的任何事务将其加倍之前增加引用。例如,以下代码在最后打印12
 
(let [x (ref 2)]
  (add-watch x nil watcher)
  (future (dosync (Thread/sleep 3000) (alter x * 2)))
  (future (dosync (Thread/sleep 3000) (alter x * 2)))
  (Thread/sleep 100) ; 确保其他事务正在运行
  (dosync (alter x inc))
  (println "end:" (deref x) (quot (System/currentTimeMillis) 1000)))

无论如何,在对 refs 和 STM 进行了一些更多阅读,以及对我的项目的业务逻辑进行了一些思考后,我得出的结论是,一两个原子对于我的使用情况可能比最初设想的大量 refs 更合适。
...