alterとcommute

Programming Clojureをcommuteの説明まで読んだので、alterとcommuteの違いが分かるようなサンプルプログラムを書いてみた。

test.clj

(def *counter* (ref 0))

(defn test-func [name time]
  (fn []
    (dotimes [i 5]
      (Thread/sleep 1000)
      (print (format "%d: I am %s. " i name))
      (dosync
       (print ".")
       (.flush *out*)
       (alter *counter* inc)
;       (commute *counter* inc)
       (println (format " --> %d" @*counter*))
       (when time
	 (Thread/sleep time))))
    (println (format "%s end" name))))

(def *t1* (Thread. (test-func "Foo" 1000)))
(def *t2* (Thread. (test-func "Bar" nil)))

(do
  (.start *t1*)
  (.start *t2*))

何をしているかと言うと、
スレッド*t1*と*t2*を作る。
それぞれのスレッドから同期化してカウンタを1増やす。
スレッド1では、以下の処理を5回繰り返す。

  1. 1秒待つ。
  2. 「I am Foo.」 を出力する。
  3. トランザクション開始
    1. 「.」 を出力する。
    2. カウンタを増やす。※alterまたはcommuteを使用する。
    3. 「--> カウンタ値」を出力する。
    4. 1秒待つ。

スレッド2は、「I am Bar.」を出力し、最後の1秒待つは無し。

関数はRunnableインターフェースを実装しているため、スレッドを書くのは簡単だ。

alterの場合の実行結果

user=> (load-file "test.clj")
nil
user=> 0: I am Foo. . --> 1
0: I am Bar. ........... --> 2
1: I am Foo. . --> 3
1: I am Bar. ........... --> 4
2: I am Foo. . --> 5
2: I am Bar. ........... --> 6
3: I am Foo. . --> 7
3: I am Bar. ........... --> 8
4: I am Foo. . --> 9
4: I am Bar. ..........Foo end
. --> 10
Bar end

※スレッド内で標準出力に出力しているため、slimeでは出力されない事に注意(出力できるかもしれないが、その方法は知らない)。
2009/11/22 訂正 スレッドからの出力は*inferior-lisp*バッファに出力される。

alter実行時に他のトランザクションにより、参照が更新されている場合はトランザクションをリトライする。
カウント値は更新後の値を表示する。
おおよそ100msec間隔でリトライしているようであるが、どこかで設定可能?

commuteの場合の実行結果

user=> (load-file "test.clj")
nil
user=> 0: I am Bar. . --> 1
0: I am Foo. . --> 2
1: I am Bar. . --> 2
2: I am Bar. . --> 4
1: I am Foo. . --> 5
3: I am Bar. . --> 5
4: I am Bar. . --> 7
Bar end
2: I am Foo. . --> 8
3: I am Foo. . --> 9
4: I am Foo. . --> 10
Foo end

Programming Clojureに書いてあるように、commute実行時は参照の更新順をSTMシステム任せにするため、上記の実行結果のようにトランザションはリトライしない。
出力されるカウント値はトランザクション終了時の値とはならない。

共有オブジェクトの更新時に、他トランザクションにより更新されていると、トランザクションをリトライするという仕様にちょっと驚いた。