Table of Contents

Var はスレッド間の 分離 によってミュータブルなストレージの安全な使用を保証しているが、トランザクショナルな参照(Ref)は software transactional memory (STM) システムによってミュータブルなストレージの安全な 共有 を保証する。Refは最初から最後まで単一のストレージに束縛され、変更をトランザクションの中でしか許容しない。

データベースのトランザクションを使用したことがあればClojureのトランザクションを理解することは容易だ - Refに対する全ての処理がアトミックで一貫性があり、分離されていることを保証する。アトミックとは、トランザクション内でのRefに対する変更が全て起こるか、一切起こらないかのどちらかであることを意味する。一貫性があるとは全ての新しい値に対して、トランザクションのコミットの前にバリデーション関数でチェックを行うことができることを意味する。分離されているとはトランザクション内での影響は、トランザクションが動いている間は他のトランザクションから観測されないことを意味する。もう一つ、STMの一般的な機能としてトランザクションの動作中に競合が起こった場合の自動的なリトライもある。

STMを実現するには複数の方式があり (ロック/悲観的, ロックフリー/楽観的 および それらの混合) 、未だに研究の対象となる問題である。ClojureのSTMは multiversion concurrency control と adaptive history queue を snapshot isolation のために使用し、固有の commute 処理を提供している。

これの現実的な意味は:

  1. Refに対する全ての読み込みは 'Refの世界' の一貫性のあるスナップショットをトランザクションの開始地点のように観測する(トランザクションの 'read point')。トランザクションはRefに対する全ての変更を 観測する 。これを in-transaction-value と呼ぶ。

  2. トランザクション内に行われたRefに対する全ての変更( ref-set, alter or commute を使用)は 「Refの世界」 のタイムライン上の一点で起こったと映る (Refの 'wirte point') 。

  3. No changes will have been made by any other transactions to any Refs that have been ref-set / altered / ensured by this transaction.

  4. このトランザクションによりコミュートされたRefは、他のトランザクションから変更を受ける かもしれないcommute を使用して適用した関数は可換であるべきなのでこれは問題にならない。

  5. リーダーとコミューターはライター、コミューター、他のリーダーを決してブロックしない。

  6. ライターはコミューターもしくはリーダーを決してブロックしない。

  7. トランザクションはリトライが 行われる のでトランザクション内でI/O等の副作用を伴う処理は避けるべきで、 io! マクロを使用することでトランザクション内で純粋ではない関数の使用を防ぐことができる。

  8. 変更を受けているあるRefの値の正当性の制約が同時に 変更を受けていない 別のRefの値に依存する場合、 ensure を使用して2つ目のRefを変更から保護することができる。 'ensure' されたRef(3つ目)は保護されるが、元のRef(2つ目)に対する変更は保護されない。

  9. Clojure MVCC STM パーシスタントなコレクションと共に良く動作するように設計されているため、Refの値にはClojureのコレクションを使用することが強く推奨される。STMのトランザクション内での処理は投機的であることから、コピーや変更のコストが低いことは必須であるためだ。パーシスタントなコレクションの場合、コピーのコストはゼロであり(不変であるため元のものを使用すれば良い)、 '変更' は構造が効率的に共有される。いずれにせよ:

  10. Ref内の値は イミュータブルである、もしくはイミュータブルとして扱われる必要がある !! そうでない場合、Clojureがあなたの助けになることはない。

この例ではユニークな数字(最初は昇順)を含むベクターに対する参照のベクターが作成される。そして複数のスレッドが起動し、繰り返しトランザクション内でランダムなベクターからランダムな位置を選択し入れ替えを行う。トランザクションを使用する以外には不可避な衝突を防ぐ工夫はしていない。

(defn run [nvecs nitems nthreads niters]
  (let [vec-refs (vec (map (comp ref vec)
                           (partition nitems (range (* nvecs nitems)))))
        swap #(let [v1 (rand-int nvecs)
                    v2 (rand-int nvecs)
                    i1 (rand-int nitems)
                    i2 (rand-int nitems)]
                (dosync
                 (let [temp (nth @(vec-refs v1) i1)]
                   (alter (vec-refs v1) assoc i1 (nth @(vec-refs v2) i2))
                   (alter (vec-refs v2) assoc i2 temp))))
        report #(do
                 (prn (map deref vec-refs))
                 (println "Distinct:"
                          (count (distinct (apply concat (map deref vec-refs))))))]
    (report)
    (dorun (apply pcalls (repeat nthreads #(dotimes [_ niters] (swap)))))
    (report)))

シャッフルの実行にあたって値の喪失や重複が起こらないことが確認できる:

(run 100 10 10 100000)

([0 1 2 3 4 5 6 7 8 9] [10 11 12 13 14 15 16 17 18 19] ...
 [990 991 992 993 994 995 996 997 998 999])
Distinct: 1000

([382 318 466 963 619 22 21 273 45 596] [808 639 804 471 394 904 952 75 289 778] ...
 [484 216 622 139 651 592 379 228 242 355])
Distinct: 1000

関連する関数

Refの作成: ref

Refの観測: deref ( @ リーダー マクロも参照)

トランザクションマクロ: dosync io!

トランザクション内でのみ使用可能: ensure ref-set alter commute

Refバリデーター: set-validator! get-validator