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 提到的内容。并且,如果您在调用 `future` 之间添加例如100毫秒的睡眠,`let` 变体也可能挂起。尽管这可能更多是一种偶然而不是规则。

1 答案

+2
 
最佳回答

我假设两个事务都失败了,并且多次重试,每次重试都非常耗时,直到其中一个偶然得到正确的时机正常运行或到达最大重试次数。

你绝对不应在事务中进行阻塞操作。

此外,你不需要确保在事务中是否修改了相同的ref。
你好,Alex,感谢你有关进行阻塞操作的警告。

我使用确保(ensure)是因为我发现当我不这么做的时候,第三个事务会在之前的任何事务都将它在倍之前增加ref。例如,下面的代码在最后打印出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更合适。
...