OCamlではCMLスタイルのスレッド間通信が可能です。 Eventモジュールには基本的なイベント型やその同期関数が揃っています。 ところが、ivarやmvarといった共有変数が無い為に苦しい状況になっています。
例えばとても単純に、整数を渡して計算結果を(今回は倍にして)返してくれるスレッドを作ってみましょう。 すぐに思いつくのは、次のようなコードです。
let start_server () = let in_ch, out_ch = new_channel (), new_channel () in let rec loop () = let x = sync (receive in_ch) in loop (sync (send out_ch (x * 2))) in ignore (Thread.create loop ()); in_ch, out_ch
let calc (in_ch, out_ch) x = wrap (send in_ch x) (fun () -> sync (receive out_ch))
これで全く問題ないです。 ただし、同期ポイントが要求を送るときになっている事に注目してください。 要求を送る時と応えを受け取る時の二種類の同期ポイント候補がありますので、仮に応えを受け取るときを同期ポイントにしたい場合には、次のようになります。
let calc (in_ch, out_ch) x = guard (fun () -> sync (send in_ch x); receive out_ch)
さぁ、ここから問題が発生します。 receive out_chが同期ポイントなので、応えを受け取るかどうかを他のイベントと選択(select)できるようになります。 すると、「ガードが発動して要求を送っているのに、receiveが選ばれない」という状況が発生します。 結果、サーバースレッドの sync (send ...が待ちに入り、サーバーがロックダウンします。
poll (send ...でいいじゃないかと思われるかもしれませんが、sync (send in_ch x)した後、たまたまreceive out_chする前にpollが発動する可能性がなきにしもあらずなので、不確実になります。 マルチスレッドプログラミングでは、この手の「微妙」なタイミング問題を排除しないと安心できません。
この問題への最後の手段としてwrap_abortがあります。
let start_server () = let in_ch, out_ch = new_channel (), new_channel () in let rec loop () = let nack, x = sync (receive in_ch) in loop (select [send out_ch (x * 2); receive nack]) in ignore (Thread.create loop ()); in_ch, out_ch
let calc (in_ch, out_ch) x = guard (fun () -> let nack = new_channel () in sync (send in_ch (nack, x)); wrap_abort (receive out_ch) (fun () -> ignore (Thread.create (fun () -> sync (send nack ())) ())))
やった、これで解決です、お疲れさまでした。 でも待ってください。毎回nackまで考えなきゃいけないんですか? それに仲介役のスレッドを起動しています。リソース的に優しくありません。
そこで、CMLではivarというスレッド間共有変数を抽象化した便利グッズが用意されています。
from http://cml.cs.uchicago.edu/pages/sync-var.html
type 'a ivar
This is the type constructor for I-structured variables. I-structured variables are write-once variables that provide synchronization on read operations. They are especially useful for one-shot communications, such as reply messages in client/server protocols, and can also be used to implement shared incremental data structures.
まさに、ivarは応えを送るのに最適と書いてあります。 実はCMLのivarは内部的にスレッドを使わずに実装されており、ivarを使う事で劇的にパフォーマンスが上がると「Concurrent Programming in ML」にも書いてありました。 ivarを使うと、次のように書けます。
let start_server () = let in_ch, out_ch = new_channel (), new_channel () in let rec loop () = let ivar, x = sync (receive in_ch) in loop (sync (Ivar.put ivar (x * 2))) in ignore (Thread.create loop ()); in_ch, out_ch
let calc (in_ch, out_ch) x = guard (fun () -> let ivar = Ivar.make () in sync (send in_ch (ivar, x)); Ivar.read ivar)
ivarすばらしい。nackもwrap_abortも要らないし、簡単じゃありませんか。 しかし、冒頭にも紹介しましたが、実はOCamlにはないのです、Ivarが。おーぃ(泣。
でも大丈夫。実装しました→concurrent cell。 OCamlでも安心してマルチスレッドプログラミングをお楽しみ下さい。 ちなみにconcurrent cellにはキューやブロードキャストやRPCなど、さらに高度なプロトコルも同梱されています。