草书:Clojure的* out *,不同的Writer,多线程时刷新和排序不一致:怎么回事?

时间:2018-11-05 17:08:18

标签: multithreading clojure buffer stdout cursive

tl; dr为什么Clojure为Writer中的线程创建了单独的newFixedThreadPool?为什么终止池后可能会刷新它?为什么只能在草书中复制该行为?

假设我们有一个应用程序,它在不同的线程中执行某些操作,并且某些内容写入stdout。假设完成所有操作后,我们要打印最终消息。

首先要遇到的是Clojure的println(如果提供了多个参数)将产生交错输出。 here对此进行了介绍。

但是似乎还有另一个问题。如果我们运行这样的内容:

(defn main []
  (let [pool (make-pool num-threads)]
    (print-multithreaded pool "Hello, world!")
    (shutdown-pool pool))
  (safe-println "All done, have a nice day."))

有时我们会

Hello, world!
All done, have a nice day.

有时

All done, have a nice day.
Hello, world!

也许在每次写完后flush

(defn safe-println [& more]
  (.write *out* (str (clojure.string/join " " more) "\n"))
  (.flush *out*))

不起作用。 有效的方法是在System.out之上借助显式Java互操作,如下所示:

(defn safe-println [& more]
  (let [writer (System/out)]
    (.println writer (str (clojure.string/join " " more)))
    (.flush writer)))

writer(PrintWriter. System/out)设为(OutputStreamWriter. System/out)

好像我们的线程中有不同的*out* ...

(def out *out*)
(defn safe-println [& more]
  (.write out (str (clojure.string/join " " more) "\n"))
  (.flush out))

有效。

所以这是问题:为什么会这样?对于Java片段,这是有道理的:System.out是静态final,因此所有线程仅存在一个实例,并且所有与之通信,因此所有事物都添加到同一缓冲区。通过打印到Clojure的*out*,主线程和池线程具有自己的*out*和缓冲区(对于主线程,它是PrintWriter;对于池线程,它是共享的{{1 }})。首先,我真的不明白为什么会这样,而且我也不清楚为什么会导致混乱的排序:我们显式地在调用 之前完成所有线程,最终调用诱导隐式OutputStreamWriter。但是,即使我们添加了显式的flush,结果也保持不变。

在这里我可能会遗漏一些非常明显的细节,如果您能帮助我,我会很高兴。如果您想查看整个可复制的示例,由于篇幅太长,在此不予赘述,以下是要点的链接:https://gist.github.com/trueneu/b8498aa259899a8fc979090fccf632de

编辑:gist的第一个版本确实有效,您必须对其进行修补才能将其破解,因此我对其进行了编辑,以从一开始就演示“不正确”的行为。

此外,要消除任何误解,这是草书的屏幕截图:https://ibb.co/jHqSL0

EDIT2:这是在原始问题中指出的,但我会强调一下。了解这种行为的意义和机制只是问题的一半。不会为每个线程创建新的flush。但是似乎正在为线程池创建一个单独的线程。 (对于此输出,将*out*减少为1,并向num-threads添加(.toString *out*)的打印。增加safe-println不会产生新的对象地址):

num-threads

EDIT3:在@glts注释之后,将(main) java.io.PrintWriter@1dcc77c6 All done, have a nice day. => nil java.io.OutputStreamWriter@7104a76f Hello, world! 更改为map。 另外,从doseq运行时,它总是产生正确的输出,这进一步使我感到困惑。因此,正如David Arenas指出的那样,行为似乎取决于上游输出处理。但是,问题仍然存在。

编辑4:大卫·阿里纳斯(David Arenas)在苹果酒中也进行了检查,无法重现其行为。似乎与Cursive的nrepl输出处理实现有关。

1 个答案:

答案 0 :(得分:1)

Clojure的*out*不会为每个线程创建一个实例(它也是静态的final),但是它确实使用了OutputStreamWriter,它没有原子保证。由于要写入单个流,因此需要同步缓冲区上的线程。

如果使用nrepl运行代码,则会看到您得到“正确”的行为。这是因为他们将 out 重新绑定到使用锁定缓冲区的自己的写程序。

nrepl的会话退出:

(defn- session-out
  "Returns a PrintWriter suitable for binding as *out* or *err*.  All of
   the content written to that PrintWriter will (when .flush-ed) be sent on the
   given transport in messages specifying the given session-id.
   `channel-type` should be :out or :err, as appropriate."
  [channel-type session-id transport]
  (let [buf (clojure.tools.nrepl.StdOutBuffer.)]
    (PrintWriter. (proxy [Writer] []
                    (close [] (.flush ^Writer this))
                    (write [& [x ^Integer off ^Integer len]]
                      (locking buf
                        (cond
                          (number? x) (.append buf (char x))
                          (not off) (.append buf x)
                          ; the CharSequence overload of append takes an *end* idx, not length!
                          (instance? CharSequence x) (.append buf ^CharSequence x (int off) (int (+ len off)))
                          :else (.append buf ^chars x off len))
                        (when (<= *out-limit* (.length buf))
                          (.flush ^Writer this))))
                    (flush []
                      (let [text (locking buf (let [text (str buf)]
                                                (.setLength buf 0)
                                                text))]
                        (when (pos? (count text))
                          (t/send (or (:transport *msg*) transport)
                                  (response-for *msg* :session session-id
                                                channel-type text))))))
                  true)))