【问题标题】:Make Clojure's println "thread-safe" in the same way as in Java以与 Java 相同的方式使 Clojure 的 println “线程安全”
【发布时间】:2013-09-06 16:22:33
【问题描述】:

在 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 的不同之处和原因,以及如何在 Clojure 中实现与 println 类似的“线程安全”行为?

【问题讨论】:

    标签: multithreading concurrency clojure


    【解决方案1】:

    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)
    

    【讨论】:

      【解决方案2】:

      在内部,println 将输出发送到写入器,该输出是 *out* 的当前绑定值。对 this 的调用不是原子的有几个原因:

      1. println 函数是多重参数。如果传递多个对象,它会多次写入*out*
      2. println 的调用被委托给称为print-method 的内部多方法(可以扩展以添加对自定义类型的打印支持)。非字符串对象(尤其是集合类型)的print-method 实现可以对*out* 进行多次写入。这与 Java 的 println 形成对比,后者将在对象上调用 .toString 并进行一次写入。

      如果您想要原子 println,您可能必须显式同步您的调用,例如:

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

      【讨论】:

      • 感谢如何的解释!但现在我知道locking 我更喜欢@noisesmith 锁定*out* 的解决方案。或者你有使用锁对象的具体原因?
      • 一般来说,我更喜欢我的代码不要锁定它不拥有的对象。你永远不知道还有什么可能锁定*out*,或者为什么。有可能没关系,但只需要一位行为不端的作家就可以锁住锁并为每个人搞砸。
      • 这解释了为什么会这样,但不能解释为什么会这样?例如,println 可能会委托一个 str 方法并在遍历所有参数后执行连接,并对 out 流执行一次写入。
      • 我认为在这种情况下锁定*out* 更有意义,因为目标是将写入同步到out,所以如果还有其他你不知道的about 对 out 有保留,您可能也希望尊重这一点,并在写入之前等待。
      【解决方案3】:

      Clojure 1.10 新增,也可以使用tap&gt; 来同步println,如下:

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

      现在您可以发送到tap&gt;,以线程安全的方式按点击接收的顺序打印:

      (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&gt; 是 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&gt; 使用 java.util.concurrent.ArrayBlockingQueue 来同步对它的调用。

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

      (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】:

        为了完整起见,使用 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 中反弹。

        【讨论】:

          【解决方案5】:

          你也可以用 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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2011-07-08
            • 2011-08-30
            • 1970-01-01
            • 1970-01-01
            • 2020-06-05
            • 1970-01-01
            • 1970-01-01
            • 2023-04-01
            相关资源
            最近更新 更多