以与Java相同的方式使Clojure的println“线程安全”

时间:2013-09-06 16:22:34

标签: multithreading concurrency clojure

在Clojure中同时调用println时,我发现它的行为与Java的System.out.println不同。

我会用Java编写什么

class Pcalls {
    public static void main(String[] args) {
        Runnable[] fns = new Runnable[3];
        for (int i = 0; i < 3; i++) {
            fns[i] = new Runnable() {
                @Override public void run() {
                    for (int i = 1; i <= 5; i++) {
                        System.out.println("Hello iteration " + i);
                    }
                }
            };
        }
        for (Runnable fn : fns) new Thread(fn).start();
    }
}

我在Clojure中解释为:

(doall (apply pcalls
              (repeat 3 #(dotimes [i 5] (println "Hello iteration" (inc i))))))

不幸的是,在Clojure版本中,输出行经常出现交错:

Hello iterationHello iteration  1
Hello iteration Hello iteration 2
Hello iteration 3
1
Hello iteration 4
1
Hello iteration Hello iteration5
 Hello iteration 2
Hello iteration 23

Hello iteration Hello iteration 4
3Hello iteration 
5
Hello iteration 4
Hello iteration 5
(nil nil nil)

在Java never happens中,每条消息都打印在自己的行上。

你能解释一下Clojure的println与Java的不同之处和原因,以及如何达到类似的线程安全&#34;在Clojure中使用println的行为?

5 个答案:

答案 0 :(得分:5)

clojure中的约定是锁定*out*,它指的是打印到的位置。

user> (doall (apply pcalls
            (repeat 3 #(dotimes [i 5]
                             (locking *out*
                               (println "Hello iteration" (inc i)))))))
Hello iteration 1
Hello iteration 1
Hello iteration 2
Hello iteration 3
Hello iteration 4
Hello iteration 5
Hello iteration 1
Hello iteration 2
Hello iteration 3
Hello iteration 4
Hello iteration 5
Hello iteration 2
Hello iteration 3
Hello iteration 4
Hello iteration 5
(nil nil nil)

答案 1 :(得分:3)

在内部,println将输出发送到作为*out*的当前绑定值的writer。有几个理由认为这不是原子的:

  1. println函数是多个arity。如果交给多个对象,则会对*out*进行多次写入。
  2. println的调用被委托给名为print-method的内部多方法(可以扩展为添加自定义类型的打印支持)。非字符串对象(尤其是集合类型)的print-method实现可以对*out*进行多次写入。这与Java的println形成对比,后者将在对象上调用.toString并进行单次写入。
  3. 如果你想要原子打印,你可能必须明确地同步你的电话,例如:

    (let [lock (Object.)]
      (defn sync-println [& args]
        (locking lock (apply println args))))
    

答案 2 :(得分:1)

Clojure 1.10中的新功能,还可以使用tap>来同步println,例如:

(add-tap println)
(tap> [1 2 3 4])
;> [1 2 3 4]

现在,您可以发送给tap>,以用线程安全的方式轻按接收的顺序打印:

(doall (apply pcalls
              (repeat 3 #(dotimes [i 5] (tap> (str "Hello iteration" " " (inc i)))))))
Hello iteration 1
Hello iteration 1
Hello iteration 2
Hello iteration 3
Hello iteration 4
Hello iteration 5
Hello iteration 1
Hello iteration 2
Hello iteration 3
Hello iteration 4
Hello iteration 5
Hello iteration 2
Hello iteration 3
Hello iteration 4
Hello iteration 5
(nil nil nil)

请注意,tap>是arity-1,因此您传递的内容不能超过一件事,这意味着在这种情况下,您必须首先使用str来串联要打印的内容。

With `tap>`, you can also have it do synchronized pretty printing:

(add-tap (bound-fn* clojure.pprint/pprint))
(tap> {:a 100 :b 200 :c 300 :d 200 :f 400 :g 400000000 :h 3992 :l {:k 10203 :f 39945 :o 29394}})

{:a 100,
 :b 200,
 :c 300,
 :d 200,
 :f 400,
 :g 400000000,
 :h 3992,
 :l {:k 10203, :f 39945, :o 29394}}

在后台,tap>使用java.util.concurrent.ArrayBlockingQueue来同步对其的呼叫。

还要注意,tap>是异步的。因此,在打印内容时,它不会阻塞。这意味着,如果您在完成打印之前退出该应用程序,它将无法完成:

(doall (apply pcalls
              (repeat 3 #(dotimes [i 5] (tap> (str "Hello iteration" " " (inc i)))))))
(System/exit 0)

"Hello iteration 1"
"Hello iteration 2"
"Hello iteration 3"
"Hello iteration 4"

答案 3 :(得分:0)

为了完整性,使用Clojure的locking的替代方法是依赖Java主机提供的System.out(默认情况下绑定到*out*)的同步。

(doall (apply pcalls
              (repeat 3 #(dotimes [i 5]
                           (.println *out* (str "Hello iteration " (inc i)))))))
(defn out-println [& args]
  (.println *out* (apply str (interpose \space args))))

但请注意Synchronization and System.out.println的答案表明,从技术上讲,Java API并不保证System.out.println的同步。当然,*out*可以在Clojure中反弹。

答案 4 :(得分:0)

您还可以使用core.async解决此问题:

(def print-chan (chan 10))

(defn aprintln [& message]
  (>!! print-chan message))

(defn start-printer! [] (thread (while true
                                  (apply println (<!! print-chan)))))

(defn do-a-thing [] (aprintln "Doing a thing"))

(defn do-another-thing [] (aprintln "Doing another thing"))

(defn -main []
  (start-printer!)
  (future (do-a-thing))
  (do-another-thing))

这将确保您的输出不会交错,无论有多少线程同时调用aprintln