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输出处理实现有关。
答案 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)))