noise

計算機科学や各種設定のメモ

OCaml の Event モジュール

OCaml threads ライブラリを使ってみました。
Thread.create まではいいのですが外部からスレッドを終了させるのは難しかったです。

なにが難しかったかというと、Thread モジュールのなかに Thread.kill : Thread.t -> unit のようなメソッドがあるのですがこれを利用しようとしてもエラーを吐いて使い物になりません。

なので Event モジュールの channel を用いて通信が必要となります。

open Batteries

let ch = Event.new_channel ()

let rec thread_0 () =
  print_endline "thread 0:";
  match Event.poll (Event.receive ch) with
    None -> begin
      Thread.delay 0.5;
      thread_0 ()
    end
    | Some _ -> begin
      print_endline "thread 0: exit";
      Thread.exit ()
    end

let rec thread_1 () =
  print_endline "thread 1:";
  Thread.delay 5.0;
  Event.poll (Event.send ch 1) |> ignore; (* ココに注目 *)
  print_endline "thread 1: exit";
  Thread.exit ()

let _ =
  let t0 = Thread.create thread_0 () in
  let t1 = Thread.create thread_1 () in
  Thread.join t1;
  Thread.join t0

これは thread_0 と thread_1 という関数を新しくスレッドとして呼び出し、thread_1 が5秒経ってから thread_0 にチャンネルを用いて終了命令(ここでは 1:int)を送っています。送受信に Event.sync を使わず Event.poll 使っているのはブロッキングして欲しくないからです。

しかし、これを実行すると意図に反して thread_0 は死んでくれません。0.5秒ごとにメッセージを出し続けています。

myhost% ocamlfind ocamlopt -thread -package batteries -linkpkg main.ml -o main
myhost% ./main
thread 0:
thread 1:
thread 0:
thread 0:
thread 0:
thread 0:
thread 0:
thread 0:
thread 0:
thread 0:
thread 0:
thread 1: exit
thread 0:
thread 0:
thread 0:
thread 0:
...

なぜでしょうか?

確定的なことは分かりませんが thread_0 が値を受信する前に thread_1 が終了してしまうとその送信イベント自体が無かったことになるように見えます。

どうやって解決するかというと単に kill メッセージを送信するときは poll じゃなくて sync を使います。

let rec thread_1 () =
  print_endline "thread 1:";
  Thread.delay 5.0;
  Event.sync (Event.send ch 1) |> ignore; (* ココ *)
  print_endline "thread 1: exit";
  Thread.exit ()

このように変更して実行すると二つのスレッドが正常に終了します。

myhost% ocamlfind ocamlopt -thread -package batteries -linkpkg main.ml -o main
myhost% ./main
thread 0:
thread 1:
thread 0:
thread 0:
thread 0:
thread 0:
thread 0:
thread 0:
thread 0:
thread 0:
thread 0:
thread 0:
thread 0: exit
thread 1: exit

まとめ

ここまでは対処療法としてのノウハウになりますが、thread kill にしか使えません。channel を相変わらず理解できていない状態です。
ということで channel についてもう少し実験して調べてみたところ次のようなことがわかりました。poll はループ内からの呼び出しです。

送信 sync sync poll poll
受信 sync poll sync poll
結果 OK OK OK NG

送信側・受信側 両方とも poll はダメ。通信が成立しません。
channel というのは待ち行列ではないようです。poll は sync を用いて受信もしくは送信の待機状態にある相手が居るかどうかを確認し居れば送受信イベントを成立させるという動作をするようです。

注意点

また次のようなコードも確認して欲しいと思います。

open Batteries

let ch = (Event.new_channel (),Event.new_channel ())

let rec thread_0 () =
  print_endline "thread 0:";
  match Event.poll (Event.receive (fst ch)) with
    None -> begin
      Thread.delay 0.5;
      thread_0 ()
    end
    | Some _ -> begin
      print_endline "thread 0: exit";
      Event.poll (Event.send (snd ch) 1) |> ignore;
      Thread.delay 3.; (* これがないと上の send が完了しない *)
      Thread.exit ()
    end

let rec thread_1 () =
  print_endline "thread 1:";
  Thread.delay 5.0;
  Event.sync (Event.send (fst ch) 1) |> ignore;
  Event.sync (Event.receive (snd ch)) |> ignore;
  print_endline "thread 1: exit";
  Thread.exit ()

let _ =
  let t0 = Thread.create thread_0 () in
  let t1 = Thread.create thread_1 () in
  Thread.join t1;
  Thread.join t0
  1. thread_1 -> thread_0 : sync/poll
  2. thread_0 -> thread_1 : poll/sync

thread_0 が thread_1 からの終了命令に対し確認(ACK)を返します。
しかし、コード内でコメントを書いている行がないと thread_0 は終了しますが thread_1 は thread_0 からのメッセージを待ち続けて終了しない状態になります。
これはスレッドの終了が早過ぎて送信イベントが発生しないためだと考えられます。
したがって、スレッド終了直前でメッセージを送信する場合は sync でなくてはならないということになります。