Clojure

並行プログラミング

今日のシステムは、多くの同時タスクを処理し、マルチコアCPUのパワーを活用する必要があります。スレッドを使用してこれを行うことは、同期化の複雑さのために非常に困難になる可能性があります。Clojureは、いくつかの方法でマルチスレッドプログラミングを簡素化します。コアデータ構造は不変であるため、スレッド間で容易に共有できます。しかし、プログラムでは状態変更が必要になることがよくあります。Clojureは実用的な言語であるため、状態の変更を許可しますが、状態が変更される場合でも一貫性を維持し、開発者がロックなどを手動で使用して競合を回避する必要がないようにするメカニズムを提供します。ソフトウェアトランザクショナルメモリシステム(STM)は、dosyncrefref-setalterなどで公開され、スレッド間で変化する状態を同期かつ調整された方法で共有することをサポートします。agentシステムは、スレッド間で変化する状態を非同期かつ独立した方法で共有することをサポートします。atomsシステムは、スレッド間で変化する状態を同期かつ独立した方法で共有することをサポートします。動的varシステムは、defbindingなどで公開され、スレッド内の状態変更を分離することをサポートします。

いずれの場合も、ClojureはJavaのスレッドシステムを置き換えるのではなく、それと連携して動作します。Clojure関数はjava.util.concurrent.Callableであるため、Executorフレームワークなどで動作します。

これの大部分は、コンカレンシーに関するスクリーンキャストでより詳細に説明されています。

Refsは、オブジェクトへの変更可能な参照です。トランザクション中に異なるオブジェクトを参照するようにref-setまたはalterできます。トランザクションはdosyncブロックで区切られます。トランザクション内のすべてのrefの変更は、すべて実行されるか、すべて実行されません。また、トランザクション内のrefの読み取りは、特定の時点での参照世界のスナップショットを反映します。つまり、各トランザクションは他のトランザクションから隔離されます。同じ参照を変更しようとする2つのトランザクション間に競合が発生した場合、一方のトランザクションが再試行されます。これらはすべて、明示的なロックなしで行われます。

この例では、整数を格納するRefsのベクトル(refs)が作成され、次に、すべてのRefをインクリメントする反復処理の数を実行するスレッドのセット(pool)が設定されます(tasks)。これは極度の競合状態を作成しますが、正しい結果が得られます。ロックは不要です!

(import '(java.util.concurrent Executors))

(defn test-stm [nitems nthreads niters]
  (let [refs  (map ref (repeat nitems 0))
        pool  (Executors/newFixedThreadPool nthreads)
        tasks (map (fn [t]
                      (fn []
                        (dotimes [n niters]
                          (dosync
                            (doseq [r refs]
                              (alter r + 1 t))))))
                   (range nthreads))]
    (doseq [future (.invokeAll pool tasks)]
      (.get future))
    (.shutdown pool)
    (map deref refs)))

(test-stm 10 10 10000)
-> (550000 550000 550000 550000 550000 550000 550000 550000 550000 550000)

一般的な用途では、refsはClojureコレクションを参照できます。Clojureコレクションは永続的で不変であるため、複数のトランザクションによる同時的な投機的な「変更」を効率的にサポートします。変更可能なオブジェクトはrefsに入れてはいけません。

デフォルトではVarsは静的ですが、メタデータを使用して定義されたVarsのスレッドごとのバインディングは、それらを動的としてマークします。動的varもまた、オブジェクトへの変更可能な参照です。defによって確立できる(スレッドで共有される)ルートバインディングがあり、*set!*を使用して設定できますが、bindingを使用して新しいストレージ場所にスレッドローカルにバインドされている場合のみです。これらのバインディングと、それらのバインディングに対するその後の変更は、バインディングブロックの動的スコープ内のコードによって、スレッド内でのみ表示されます。ネストされたバインディングはスタックプロトコルに従い、制御がバインディングブロックから抜けるとアンワインドされます。

(def ^:dynamic *v*)

(defn incv [n] (set! *v* (+ *v* n)))

(defn test-vars [nthreads niters]
  (let [pool (Executors/newFixedThreadPool nthreads)
        tasks (map (fn [t]
                     #(binding [*v* 0]
                        (dotimes [n niters]
                          (incv t))
                        *v*))
                   (range nthreads))]
      (let [ret (.invokeAll pool tasks)]
        (.shutdown pool)
        (map #(.get %) ret))))

(test-vars 10 1000000)
-> (0 1000000 2000000 3000000 4000000 5000000 6000000 7000000 8000000 9000000)
(set! *v* 4)
-> java.lang.IllegalStateException: Can't change/establish root binding of: *v* with set

動的varは、介入する呼び出しの引数リストと戻り値を汚染することなく、呼び出しスタック上の異なるポイント間で通信する方法を提供します。さらに、動的varはコンテキスト指向プログラミングの一種をサポートします。defnで定義された関数はvarに格納されるため、それらも動的に再バインドできます。

(defn ^:dynamic say [& args]
  (apply str args))

(defn loves [x y]
  (say x " loves " y))

(defn test-rebind []
  (println (loves "ricky" "lucy"))
  (let [say-orig say]
    (binding [say (fn [& args]
                      (println "Logging say")
                      (apply say-orig args))]
      (println (loves "fred" "ethel")))))

(test-rebind)

ricky loves lucy
Logging say
fred loves ethel