我使用以下代码作为一个处理我的请求的一部分进行日志记录.这个代码已经多次出现过了.当我进行多个并行调用时,由于此代码,我遇到了死锁.
(defn log [msg & vals]
(let [line (apply format msg vals)]
(locking System/out (println line))))
Run Code Online (Sandbox Code Playgroud)
任何人都知道这里可能出现的问题.
谢谢
我猜想死锁是由于log与其他代码的交互造成的; 特别是,在REPL进行测试时可以预期,原因我在下面说明.(意思是直接Clojure REPL,而不是lein repl其他基于nrepl的repl等)
涉及的关键问题与同步on有关System/out,这说明了一个更广泛的观点,即同步JDK或Clojure本身提供的对象并不是一个好主意,因为这可能会很好地干扰涉及这些对象的现有锁定协议(其实与Clojure的情况*out*和System/out,因为我们很快就会看到).这说明的另一点是锁不构成.
答案从提供解决方案开始,然后才详细介绍Clojure打印中涉及的锁定协议,因为后面的讨论有点偏长,基本建议可以非常简洁地说明.
查看这种情况的一种方法是,如上所述,同步核心JVM类或Clojure提供的对象并不是一个好主意,因为这可能会干扰这些对象的锁定协议.已经是的一部分.相反,人们可以随时引入新的哨兵对象,然后拥有并同步它们:
(def ^:private log-sentinel (Object.))
(defn log [msg & vals]
(let [line (apply format msg vals)]
(locking log-sentinel
(println line)))
Run Code Online (Sandbox Code Playgroud)
您仍然可以以输出交错的形式受到不相关打印输出的干扰,但大多数情况下您根本不应该有任何此类打印输出(除了REPL提示打印输出,其中出现故障不应该是一个太大的问题;调试打印输出也可以使用log,无论如何,那些将在生产中关闭,对吧?),否则你可能只是喜欢将日志输出打印到不同的输出流.
另外,由于不久将要讨论的原因,在这种情况下,甚至不必使用自己的锁,只要用一个print代替println; 这个print调用只接受一个参数的事实是关键:
(defn log [msg & vals]
(let [line (str (apply format msg vals) "\n")]
(print line)))
Run Code Online (Sandbox Code Playgroud)
flush如果要立即打印输出,可以在最后添加呼叫.(然后有可能flush在另一个线程之后发生print,但无论如何打印输出都会很快发生.)
这个版本log是我推荐的版本.这可能是最简单的解决方案; 此外,它还可以保护您的打印输出不会与通过Clojure打印功能的任何其他打印输出交错.
注意事项:据我所知,我在下面描述的行为在JDK文档的任何地方都没有提及,因此任何对它的依赖都需要您自担风险(尽管这可能不是一个可怕的风险.)
在这个特殊情况下,值得注意的是,*out*已经有一个锁定协议,它保证来自print和朋友的输出的各个位(例如它们各自参数的表示,它们之间添加的空格和由prn/ 添加的新行println)将不是交错的.
这种方式的工作方式是*out*默认存储java.io.OutputStreamWriter包装java.lang.System.out,也称为System/out.此java.io.OutputStreamWriter实例(实际上是任何java.io.Writer实例)在受保护lock字段中存储在执行写入时它同步的对象.在这种情况下*out*,恰好是那个对象System/out.print朋友们只是*out*逐个地提供他们的参数(和插入空间),因此,如上所述,任何单个参数都不会与输出中的其他数据交错,但单个print调用的几个参数可能会被分开.因此,构建一个字符串然后打印它是线程安全的,而多参数print在更简单的场景中很方便.
System/out在REPL处锁定时死锁的原因在这一点上,我想重申,我认为避免使用内置对象进行同步的锁定协议(1)在任何情况下都是一个好主意,(2)应该有希望现在解决你的问题,(3)是某种东西我可以推荐,而无需询问有关您的代码库的更多详细信息.在描述为什么在REPL期望这种行为.以下讨论适用于直接Clojure REPL,而不是lein repl等.
首先,synchronized(Java)/ locking(Clojure)获得的锁是可重入的,这解释了log在单线程使用中函数没有问题的事实- 很明显,一旦控件到达locking表单的主体,当前线程将能够line成功打印(因为它已经保存了System/out监视器).
引入死锁涉及System/out很简单:
(locking System/out
@(future (println :foo))) ; note the @ !
Run Code Online (Sandbox Code Playgroud)
通过中间的函数调用,系统可能会或可能不会死锁:
(defn f [fut]
(locking System/out
@fut))
;; will deadlock or not depending on whether the future is quick enough
(f (future (println :foo)))
Run Code Online (Sandbox Code Playgroud)
扩展上面代码片段中的注释,如果将来无法完成所有打印(这里涉及新:foo行,可能是刷新操作,尽管最后一部分取决于当前值*flush-on-newline*),然后f获取锁定System/out,它和f线程将陷入僵局.如果未来的打印速度非常快,那么在f获得锁定之前就会完成,一切都会好的.
现在,在REPL中工作时,可能出现类似情况:
(defn f [i]
(locking System/out
(println :foo i)))
(dotimes [i 10]
(future (f i)))
Run Code Online (Sandbox Code Playgroud)
这种僵局在我的机器上始终没有打印任何东西.随着迭代次数达到10000次,在死锁之前,它往往会进入相当多的打印输出,每个打印输出都在自己的行上.
相反,
(defn f [i]
(println :foo i))
(dotimes [i 10]
(future (f i)))
Run Code Online (Sandbox Code Playgroud)
打印出应有的一切,但"没有特别的顺序"; 特别是,下一个提示发生在某个任意位置,通常不在打印文本的末尾附近.
请注意,尽管在任何情况下都会打印任何内容,但每个单独的项目(:foo整数,空格,换行符)都是单独打印的(没有交错).当然,这是由于上述锁定所致*out*.