Ch.10 SML#の拡張機能:Cとの直接連携

§ 10.5. マルチスレッドプログラミング

SML#は独自のスレッドプリミティブを用意していませんが, C関数のインポート機能とコールバック機能を組み合わせ, POSIXスレッドライブラリをインポートすることで, SML#でネイティブなマルチスレッドプログラミングを行うことができます.

SML#は,POSIXスレッドライブラリに対して,何か特別な ことをしているわけではありません. SML#のコールバック機能は,C関数を呼び出したスレッドとは 異なるスレッドからコールバックされたとしても,期待通りに動くように 設計されています. そのため,SML#では,スレッドを生成する可能性のあるどの C関数も,ただそのままインポートし呼び出すだけで,スレッド生成機能を含めた 全ての機能を,SML#から活用することができます. 従って,POSIXスレッドライブラリに限らず,例えば別スレッドで 非同期的にコールバック関数を呼び出すサウンドプログラミングライブラリ なども,SML#にインポートし,コールバックルーチンをSML#で 書くことができます. もちろん,ユーザーが作成した独自のスレッドライブラリをSML#から 使用することもできます.

それでは,POSIXスレッドライブラリをインポートして,SML#から ネイティブスレッドを使用してみましょう. まず,スレッドのハンドルの型pthread_tに対応するSML#の 型を決める必要があります. このマニュアルの執筆時点では,pthread_tはLinuxでは unsigned long int,Mac OS Xではポインタと定義されています. どちらのプラットフォームでも,pthread_tはポインタと同じ大きさ を持つ不透明な基本型と見なすことができますので, SML#ではpthread_tを以下のように定義することにします.

type pthread_t = unit ptr

スレッドを生成するpthread_create関数の型は,以下のように インポートすることができます.

val pthread_create =
    _import "pthread_create"
    : (pthread_t ref, unit ptr, unit ptr -> unit ptr, unit ptr) -> int

この関数を呼び出しスレッドを生成する関数spawnを書きましょう. 第2引数はスレッドの属性, 第4引数はコールバック関数に渡す引数ですが, ここでは使用しないので,これらにはNULLを渡すことにします. NULLポインタはPointer.NULL ()で得られます. 従って,spawn関数は以下のように定義できます(エラー処理 は省略します).

fun spawn f =
    let
      val r = ref (Pointer.NULL ())
    in
      pthread_create (r,
                      Pointer.NULL (),
                      fn _ => (f () : unit; Pointer.NULL ()),
                      Pointer.NULL ());
      !r
    end

同様に,スレッドの終了を待つ関数pthread_joinも,簡単に インポートできます.

val pthread_join =
    _import "pthread_join"
    : (pthread_t, unit ptr ref) -> int
fun join t =
    (pthread_join (t, ref (Pointer.NULL ())); ())

マルチスレッドプログラミングの例として, これらの関数を組み合わせ,時間のかかる計算を別スレッドで 行うプログラムを書いてみましょう. 以下は,fib 42をバックグラウンドで計算するプログラムを, 対話モードで書いた例です.

# fun fib 0 = 0 | fib 1 = 1 | fib n = fib (n - 1) + fib (n - 2);
val fib = fn : int -> int
# val r = ref 0;
val r = ref 0 : int ref
# val t = spawn (fn () => r := fib 42);
val t = ptr : unit ptr
# join t;
val it = () : unit
# !r;
val it = 267914296 : int