为什么阻塞导致锁定

时间:2013-08-10 03:42:18

标签: clojure

我使用以下代码进行日志记录,作为处理我的请求的一部分。这个代码已经多次出现过了。当我进行多个并行调用时,由于此代码,我遇到了死锁。

(defn log [msg & vals]
  (let [line (apply format msg vals)]
    (locking System/out (println line))))

任何人都知道这里可能出现的问题。

由于

2 个答案:

答案 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/outprint和朋友只是将他们的参数(和插入空格)逐个地提供给*out*,因此,如上所述,保护任何单个参数不与输出中的其他数据交错,但是有几个参数单个print电话可能会被分开。因此,构建一个字符串然后打印它是线程安全的,而多参数print在更简单的场景中很方便。

在REPL

处锁定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的显示器”对我来说并不是很清楚。

对于试验,尝试删除对(锁定)函数的调用,看看你的系统是否继续工作。