我使用以下代码进行日志记录,作为处理我的请求的一部分。这个代码已经多次出现过了。当我进行多个并行调用时,由于此代码,我遇到了死锁。
(defn log [msg & vals]
(let [line (apply format msg vals)]
(locking System/out (println line))))
任何人都知道这里可能出现的问题。
由于
答案 0 :(得分:5)
我猜想死锁是由于log
与其他代码的互动造成的;特别是,在REPL进行测试时可以预期,原因我在下面说明。 (意思是直接Clojure REPL,而不是lein repl
,其他基于nrepl的repl等。)
涉及的关键问题与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)))
你仍然可以以输出交错的形式受到不相关打印输出的干扰,但大多数时候你根本就不应该有任何这样的打印输出(除了REPL提示打印输出,其中的打印输出不应该是'太麻烦了;调试打印输出也可以使用log
,无论如何,那些将在生产中关闭,对吧?),否则你可能只是喜欢将日志输出打印到不同的输出流。
此外,由于不久将要讨论的原因,在这种情况下,甚至不必使用自己的锁,只要使用print
代替println
; print
调用只接受一个参数的事实是关键:
(defn log [msg & vals]
(let [line (str (apply format msg vals) "\n")]
(print line)))
如果您想立即打印输出,可以在最后添加对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
时出现死锁的原因
此时我想重申,我认为避免使用内置对象进行同步的锁定协议(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 @ !
中间有一个函数调用,系统可能会或可能不会死锁:
(defn f [fut]
(locking System/out
@fut))
;; will deadlock or not depending on whether the future is quick enough
(f (future (println :foo)))
扩展上面代码片段中的注释,如果将来未能完成所有打印(这里涉及:foo
,换行和可能是刷新操作,尽管最后一部分取决于当前值在*flush-on-newline*
取得f
上的锁定之前,System/out
}的f
,它和f
的线程将会死锁。如果未来的印刷速度非常快,那么在(defn f [i]
(locking System/out
(println :foo i)))
(dotimes [i 10]
(future (f i)))
获得锁定之前就可以完成,并且一切都会好的。
现在,在REPL中工作时,可能出现类似的情况:
(defn f [i]
(println :foo i))
(dotimes [i 10]
(future (f i)))
这种情况在我的机器上一直没有打印任何东西。随着迭代计数达到10000,它会在死锁之前进入相当多的打印输出,每个打印输出都在自己的行上。
相比之下,
:foo
打印出它应该的所有内容,但"没有特定的顺序&#34 ;;特别是,下一个提示发生在某个任意位置,通常不会在打印文本的末尾附近。
请注意,尽管在任何情况下都会打印任何内容,但每个单独的项目(*out*
,整数,空格,换行符)都是单独打印的(没有交错)。当然,这是由{{1}}执行上述锁定所致。
答案 1 :(得分:-1)
来自locking文档:
在保持x的监视器的同时,在隐式do中执行exprs。 将在所有情况下释放x的监视器。
您正在持有System.out的监视器,当您有多个并行调用时,您将获得死锁异常。
虽然“会在所有情况下释放x的显示器”对我来说并不是很清楚。
对于试验,尝试删除对(锁定)函数的调用,看看你的系统是否继续工作。