Make Clojure's println “thread-safe” in the same way as in Java

一笑奈何 提交于 2021-02-08 07:28:32

问题


While playing around with concurrent calls to println in Clojure I found that its behaviour is different from Java's System.out.println.

What in Java I would write

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();
    }
}

I paraphrased in Clojure as:

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

Unfortunately, in the Clojure version the output lines often appear interleaved:

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)

In Java this never happens, every message is printed on its own line.

Can you explain how and why Clojure's println differs from Java's, and how to arrive at a similar kind of "thread-safe" behaviour with println in Clojure?


回答1:


Internally, println sends output to the writer that is the currently-bound value for *out*. There are a couple of reasons that calls to this are not atomic:

  1. The println function is multiple arity. If handed multiple objects, it makes multiple writes to *out*.
  2. Calls to println are delegated to an internal multimethod called print-method (which can be extended to add print support for custom types). The print-method implementation for non-string objects, especially collection types, can make multiple writes to *out*. This is in contrast to Java's println which will call .toString on the object and make a single write.

If you want atomic println's, you'll probably have to explicitly synchronize your calls, e.g.:

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



回答2:


A convention in clojure is to lock *out*, which refers to the location printed to.

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)



回答3:


New in Clojure 1.10, one can also make use of tap> to synchronize println, as such:

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

Now you can send to tap> to print in the order tap receives in a thread safe manner:

(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)

Just note that tap> is arity-1, so you can't pass it more than one thing, which means in this case you have to use str first to concatenate what you want printed.

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}}

Under the hood, tap> uses a java.util.concurrent.ArrayBlockingQueue to synchronize the calls to it.

Also note that tap> is asynchronous. So while things are being printed, it won't block. That means that if you quit the app before it is done printing, it won't finish:

(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"



回答4:


For completeness, an alternative to using Clojure's locking is to rely on the synchronisation of System.out (to which *out* is bound by default) provided by the Java host.

(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))))

But note that the answers to Synchronization and System.out.println suggest that technically the Java API does not guarantee synchronisation for System.out.println. And of course *out* can be rebound in Clojure.




回答5:


You can also solve this problem with 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))

This will ensure that your outputs don't interleave, no matter how many threads call aprintln at once.



来源:https://stackoverflow.com/questions/18662301/make-clojures-println-thread-safe-in-the-same-way-as-in-java

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!