プログラミング言語SML#解説 4.0.0版
11 SML#の拡張機能:マルチスレッドプログラミング

11.1 Pthreadsプログラミング

OSが提供するPthreadsライブラリを ほぼそのままバインドしたPthreadストラクチャが 標準で提供されています. スレッドを生成するpthread_create関数および 結合するpthread_join関数は以下の名前と型で提供されます.

Pthread.Thread.create : (unit -> int) -> Pthread.thread
Pthread.Thread.join : Pthread.thread -> int

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

# fun fib 0 = 0 | fib 1 = 1 | fib n = fib (n - 1) + fib (n - 2);
val fib = fn : int -> int
# val t = Pthread.Thread.create (fn _ => fib 42);
val t = ptr : Pthread.thread
# Pthread.Thread.join t;
val it = 267914296 : int

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

Pthreadライブラリをインポートして,SML#から使ってみましょう. まず,スレッドのハンドルの型pthread_tに対応するSML#の 型を決める必要があります. このマニュアルの執筆時点では,pthread_tはLinuxでは unsigned long int,macOSではポインタと定義されています. どちらのプラットフォームでも,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 ())); ())

このようにして定義したspawnjoinを使って この節冒頭の例と同じプログラムを対話モードで書くと以下のようになります.

# 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
# fun g () = r := fib 42;
val g = unit -> unit
# val t = spawn g;
val t = ptr : unit ptr
# join t;
val it = () : unit
# !r;
val it = 267914296 : int

なお,

# val t = spawn g;

の行を

# val t = spawn (fn () => r := fib 42);

と書くことは危険です. なぜなら,ガベージコレクタが関数式の生成するクロージャを 別スレッドで関数が呼び出される前に回収してしまう可能性があるからです. ガベージコレクションの影響については29.2節を 参照してください.