For Development HEAD DRAFTSearch (procedure/syntax/module):

9.35 gauche.threads - スレッド

Gaucheでは、POSIXスレッド(pthreads)か Windowsスレッド上に構築されるスレッドを使うことができます (どちらを使うかはプラットフォームによります)。

Module: gauche.threads

スレッドを扱うAPIを提供します。スレッドサポートは組み込みで、 このモジュールは名前空間を提供しているだけです。

Schemeレベルでは、スレッドサブシステムの違い(pthreadsかWindowsスレッドか)を 意識する必要はほどんど無いでしょう。ただ、特定のサブシステムのセマンティクスに 依存したコードを書きたい場合は、 pthreadプラットフォームとWindowsスレッドプラットフォームで それぞれ、gauche.sys.pthreadsgauche.sys.wthreads という機能識別子が用意されています。 cond-expandでプラットフォーム依存のコードを切り替えられます (機能条件式参照)。

コンパイル時ではなく実行時にスレッドが有効かどうかをチェックするためには、 次の手続きを使います。

Function: gauche-thread-type

{gauche.threads} サポートされているスレッドのタイプを表すシンボルを返します。 POSIX pthreadsならpthread、 Windowsスレッドならwthreadが返されます。

(註:pthreadプラットフォームではシンボルpthreadではなく pthreadsを返すべきでした。そうすれば、 gauche-thread-typeの返り値はコンフィグレーション時に --enable-threadsに与えた値と対応するものになっていたでしょう。 互換性のために、残念ながらこの見過ごしは修正されないでしょう。

SchemeレベルのスレッドAPIはSRFI-18、“マルチスレッドサポート”を満たし、 Gaucheのオブジェクトのインターフェースでラップされます。


9.35.1 スレッドプログラミングTips

Gaucheのスレッドをどう使うか

スレッドのAPIは外見上シンプルでポータブルに見えますが、 その機能の潜在的な力を活用するためには、スレッドがどのように 実装されているかを知る必要があります。 いくつかの言語では言語組み込みの機能としてスレッドをサポートし、 プログラマによるスレッドの利用を推奨しています。 しかし、多くの場合、実現したいアルゴリズムをスレッドを使わずに 実装する方法があります。 スレッドを使うことの利点と欠点を、そのスレッドがシステムによってどのように 実現されているかを考慮した上で比較する必要があります。

Gaucheでは、スレッドを使う一番の目的は、他の方法で表現することが 難しい、プリエンプティブなスケジューリングを必要とする プログラムを書くことです。プリエンプティブなスレッドは、 例えば、中断できないブロッキングI/Oを行うモジュールを 使わなければならないときや、実行時間の分からない計算に 割り込みを行いたいときなどに必要となります。

それぞれのGaucheのスレッドには、個別の仮想マシンが割り当てられ、 専用のPOSIXスレッドにより実行されます。したがって、コンテキスト スイッチのオーバヘッドは、ネイティブスレッドと同等です。 しかし、スレッドの生成は、例えばcall/ccによる軽量スレッドよりは ずっとコストのかかる処理です。 このように、Gaucheのプリエンプティブなスレッドは、 きめ細かい計算のために幾千ものスレッドを生成したいアプリケーション 向けではありません

推奨される使用方法は、いわゆる“スレッドプール”と呼ばれる テクニックです。つまり、スレッドの集合を作って長時間それを 保持し、必要になったときにジョブをそこへディスパッチする というものです。Gaucheはcontrol.thread-poolモジュール (control.thread-pool - スレッドプール参照) でスレッドプールの実装を提供しています。

プリエンプティブなスレッドには他にも難しい点があり、 しばしばネイティブなプリエンプティブスレッドよりも より良くフィットする代替策があります (例えばhttps://www-sop.inria.fr/mimosa/rp/FairThreads/html/FairThreads.html参照)。

もちろん、これらのテクニックはネイティブスレッドとは相互排他ではありません。 例えば、“スレッドプール”テクニックと一緒にディスパッチャを使うこともできます。 それらの機能を実現するために、ネイティブスレッドが唯一の方法ではないということを 心に留め置いて下さい。

スレッドボディ内で捕捉されないエラー

シングルスレッドのプログラムが予期せぬ(捕捉されない)エラーを起こした場合、 デフォルトではGaucheはエラーメッセージとスタックトレースを表示します。 ところが、スレッドで捕捉されないエラーが生じスレッドが終了しても、 何も表示されません。

それはこういうわけです。スレッド本体でエラーが起き、それがスレッド中で処理されなければ、 スレッドは終了し、エラー自体はそのスレッドの終了を待つスレッドへと伝達される仕組みだからです。 エラーで終了したスレッドに対してthread-join!を呼んだ時点で、 元のエラーを<uncaught-exception>でラップしたエラーが投げられます。 この動作はSRFI-18で規定されています。

新たなスレッドを一時的な計算のために走らせて、その結果をthread-join!で 受け取るという使い方なら、この動作は便利です。「親スレッド」でまとめてエラーを 捕捉できるからです。けれどもスレッドをずっとループさせてジョブを次々と処理する、といった 用法ではthread-join!は呼ばれるとは限らず、この仕様は落とし穴になり得ます。 スレッドがエラーで死んだのに気づかないかもしれないからです。 (エラーで終了したスレッドがthread-join!されることなくGC対象になった場合は 警告が出力されます。しかし、GCがどのタイミングで起きるかはわかりません。)

そういう使い方をするスレッドについては、本体を常にguardで包んで エラーを明示的に処理すべきです。例えばエラーメッセージとスタックトレースを出したければ report-errorを呼びます。

(thread-start!
 (make-thread (^[] (guard (e [else (report-error e) #f])
                     ... thread body ...))))

スレッドでの例外の処理について詳しくはスレッド例外を参照してください。

註: 0.9.5の時点では、guardのエラーハンドラ節が 末尾コンテクストで呼ばれないというバグがあります。 例えば次のコードはSchemeでは本来スタックを消費せずにループするはずですが、 現在のGaucheではスタックを徐々に食いつぶしてしまいます。

(thread-start!
 (make-thread (^[] (let loop ()
                     (guard (e [else (report-error e) (loop)])
                       ... thread body ...)))))

当面は回避策として、loopの呼び出しをguardの外側に出してください。

(thread-start!
 (make-thread (^[] (let loop ()
                     (guard (e [else (report-error e)])
                       ... thread body ...)
                     (loop)))))

9.35.2 スレッド手続き

Builtin Class: <thread>

スレッドを表すクラスです。それぞれのスレッドは、POSIXスレッドにより 評価される関連付けられた手続きを持ちます。手続きが正常に戻ると、 その結果は内部的な“結果”スロットに格納され、thread-join!により 取得することができます。例外が投げられるか thread-terminate!により終了されるかで、手続きが異常終了すると、 例外条件が内部的な“結果としての例外”スロットに格納され、 その例外条件は終了したスレッドに対してthread-join!を 呼ぶスレッドへと渡されます。

それぞれのスレッドは独自の動的環境と動的なハンドラスタックを持っています。 あるスレッドが生成されると、その動的な環境は生成者の動的な環境によって 初期化されます。そのスレッドの動的なハンドラスタックは初期においては空です。

スレッドは以下の4つの状態のうちのひとつを取ります。thread-state手続きで スレッドの状態を調べることができます。

new

まだ作られたばかりで起動されてない状態です。make-threadが返すスレッドは この状態です。スレッドがひとたび起動されると、そのスレッドがこの状態に戻ることは 決してありません。 この時点ではPOSIXスレッドはまだ作られません。thread-start!によって POSIXスレッドが作られ、Gaucheのスレッドを実行します。

runnable

thread-start!によって起動されたスレッドはこの状態になります。 スレッドがシステムコールによるブロックされている時もその状態はrunnable であることに注意してください。

stopped

スレッドがthread-stop!によって止められるとこの状態になります。 この状態のスレッドはthread-cont!によって再びrunnableになり、 止められた時点から実行を再開することができます。

terminated

割り当てられたコードの実行が終了したり、thread-terminate!によって 強制的に終了させられた時に、スレッドはこの状態になります。 一度この状態になると他の状態に遷移することはありません。

複数のスレッドで共有されるリソースへのアクセスは、同期化プリミティブにより 明示的に保護されなければなりません。同期プリミティブ参照。

ポートへのアクセスはGaucheによりシリアライズされます。 複数のスレッドが1つのポートへの書き込みを試みた場合、それらの出力は 混じることもありますが、失われる出力はなく、そのポートのステータスは 一貫性が保たれます。複数のスレッドが1つのポートからの読み込みを試みた 場合、1つの読み込みプリミティブ(例えば、readread-charread-lineなど)がアトミックに実行されます。

シグナルハンドラは全てのスレッドで共有されますが、それぞれのスレッドは 独自のシグナルマスクを持ちます。詳細は、シグナルとスレッドを参照。

スレッドオブジェクトは以下の外部スロットを持ちます。

Instance Variable of <thread>: name

スレッドに関連付けられる名前。 これは単にアプリケーションにとっての便宜を図るためのものです。 原始となるスレッドは“root”という名前を持ちます。

Instance Variable of <thread>: specific

アプリケーションが使うスレッドローカルなスロット。

Function: current-thread

[SRFI-18], [SRFI-21]{gauche.threads} 現在のスレッドを返します。

Function: thread? obj

[SRFI-18], [SRFI-21]{gauche.threads} objがスレッドなら#t、そうでなければ#fを返します。

Function: make-thread thunk :optional name

[SRFI-18], [SRFI-21]{gauche.threads} thunkを実行するための新しいスレッドを生成して返します。 そのスレッドの実行を開始するには、thread-start!を呼ぶ必要があります。 thunkの実行結果は、thread-join!を呼ぶことで回収できます。

オプション引数nameを与えることで、そのスレッドに名前を与えることができます。

作成されたスレッドは、呼び出したスレッドのシグナルマスクを継承し (シグナルとスレッド参照)、また呼び出したスレッドの持つ その時点でのパラメータのコピーを受けとります。

これらの初期化操作以外に、作られるスレッドと呼び出したスレッド間の関係は ありません。Unixのプロセスのような親子関係があるわけではないのです。 どのスレッドも、他のスレッドに対してthread-join!を発行 して結果を受け取ることができます。 もし誰もthread-join!を発行せず、また作られたスレッドに対する 参照を保持していなかった場合、スレッドは実行が終了した後にガベージコレクトされます。

もしスレッドが捕捉されない例外のために実行を終了し、その結果がthread-join!で 回収されなかった場合、標準エラーポートに“thread dies a lonely death” という 警告メッセージが出力されます。そのようなケースは通常何らかのコーディングエラーで あるからです。スレッドの結果を回収しない場合は、 thunk中ですべての例外を捕捉し処理しなければなりません。

内部的に、この手続きは単にSchemeスレッドオブジェクトを割り当て初期化している だけです。POSIXスレッドはthread-start!が呼ばれるまで生成されません。

Function: thread-state thread

{gauche.threads} threadの状態を示す、newrunnablestoppedterminatedのいずれかのシンボルを返します。

Function: thread-name thread

[SRFI-18], [SRFI-21]{gauche.threads} threadのスロットnameの値を返します。

Function: thread-specific thread
Function: thread-specific-set! thread value

[SRFI-18], [SRFI-21]{gauche.threads} threadの指定したスレッドの値を取得/設定します。

Function: thread-start! thread

[SRFI-18], [SRFI-21]{gauche.threads} threadを開始します。threadが “new” 状態でなければエラーが投げられます。 threadを返します。

Function: thread-try-start! thread

{gauche.threads} threadが “new” 状態であればそれを開始し、thread自身を返します。 そうでなければ#fを返します。

スレッドは開始する前に “terminated” 状態になる場合があります。他のスレッドが そのスレッドに対してthread-terminate!を呼んだ場合です。 開始する前に終了してしまったスレッドに対してthread-start!を呼ぶとエラーが 投げられますが、この手続きではエラーになりません。

Function: thread-join! thread :optional timeout timeout-val

[SRFI-18], [SRFI-21]{gauche.threads} threadの終了、あるいはtimeoutが与えられていればtimeoutが それに達するのを待ちます。

Timeoutは絶対的な時間を表す<time>オブジェクト(時間参照)か、 この手続きが呼ばれた時刻からの相対的な時間を秒数で表した実数でなければなりません。 タイムアウトが指定されていない(デフォルト)は#fです。

threadが正常に終了したら、thread-join!threadの 結果フィールドに格納されている値を返します。 threadが異常終了したら、thread-join!threadの結果例外 フィールドに格納されている例外を投げます。それは <terminated-thread-exception><uncaught-exception>のどちらかです。

タイムアウトに達すると、timeout-valが与えられていればtimeout-valを返し、 与えられていなければ<join-timeout-exception>を投げます。

これらの例外の詳細についてはスレッド例外を参照してください。

Function: thread-yield!

[SRFI-18], [SRFI-21]{gauche.threads} 呼び出しているスレッドの実行を中断し、他に待機中の実行可能なスレッドがあれば、 CPUにそれを処理させます。

Function: thread-sleep! timeout

[SRFI-18], [SRFI-21]{gauche.threads} 呼び出しているスレッドをtimeoutに指定した時間だけ中断します。 timeoutは絶対的な時間を表す<time>オブジェクト(時間参照)か、 この手続きが呼ばれた時刻からの相対的な秒数を表す実数でなければなりません。

指定された時間が経過すると、thread-sleep!は未定義値を返します。

timeoutが過去の時間を指していたら、thread-sleep!はすぐに戻ります。

Function: thread-stop! thread :optional timeout timeout-val

{gauche.threads} 対象となるスレッドthreadの実行を一時的に停止します。 停止されたスレッドはthread-cont!により実行を再開させられます。

停止リクエストは同期的に処理されます。すなわち、GaucheのVMは実行ループ中の 「安全な」場所で停止リクエストをチェックし、それがあれば自分自身を停止するように なっています。このことは、threadがシステムコールによりブロックされている場合、 それが戻ってくるまでスレッドがstopped状態にならないことを意味します。

デフォルトでは、thread-stop!は対象スレッドが停止状態になってから 戻ってきます。しかしそうなるまでにどれだけ時間がかかるかわからないので、 省略可能引数timeoutを与えて、タイムアウトすることができます。 timeout引数には#f(タイムアウトせず停止するまで待ち続ける)、 <time>オブジェクト(絶対的な時刻を指定)、もしくは 実数(現在からの秒数を指定)を与えられます。

thread-stop!の戻り値は、対象スレッドを停止させられたなら そのthreadオブジェクト、タイムアウトしたならtimeout-valです。 timeout-valが省略された場合は#fとみなされます。

対象スレッドが既に呼び出しスレッドの要求によって停止していた場合は、 直ちにthreadが返されます。

thread-stop!がタイムアウトしてthread-stop!から帰ってきた 場合でも、リクエストは有効なままになっており、未来のいつかthreadを 停止させます。呼び出し側は、いずれthread-stop!を再び呼び出して 停止を確認する必要があります。

対象スレッドが既に別のスレッドによって停止させられていた場合 (別のスレッドによって停止要求が出されて停止待ちに なっている状態も含みます)や、 対象スレッドがrunnableでもstoppedでもない状態の場合は エラーが報告されます。

Function: thread-cont! thread

{gauche.threads} thread-stop!で停止したthreadの実行を再開します。 threadが停止状態でなかったり、thread-cont!を呼び出したのとは 別のスレッドによって停止させられていた場合はエラーが報告されます。

停止要求を既に出していて、タイムアウトしていた場合、thread-cont!は その停止要求をキャンセルする役割を持ちます。

Function: thread-terminate! thread :key force

[SRFI-18+], [SRFI-21+]{gauche.threads} 指定されたスレッドthreadを終了します。 threadは終了され、<terminated-thread-exception>のインスタンスが threadの結果例外のフィールドに格納されます。

threadが呼び出しているスレッドと同じ場合、この手続きは戻りません。 そうでなければ、この手続きは未定義値を返します。

threadが既に終了していた場合は、この手続きは何もしません。

デフォルトでは、この手続きは指定スレッドを「安全な」箇所で終了させます。 これは、プロセスの状態に最低限の一貫性を保証します。 ただし、指定スレッドがロックしていたmutexはロックされたまま「放棄された」状態になります。 他のスレッドがそのmutexに触ろうとすると<abandoned-mutex-exception>例外が 投げられます。

この戦略では、スレッドがある種のシステムコールでブロックしていた場合、 Schemeレベルのスレッドが「terminated」とマークされた後でもシステムスレッドが 生きたまま残る可能性があります。このシステムスレッドが再開されてGaucheのランタイムに 制御が戻れば、その時点でシステムスレッドは自己終了します。

もし何らかの理由でシステムスレッドも確実に終了させたい場合は、 forceキーワードに真の値を渡してください。その場合、 thread-terminate!は通常の穏やかな終了を試みて失敗したら、 強制的にシステムスレッドを終了させます。 これは最終的にpthread_cancelTerminateThreadコールによって 行われ、スレッドに後始末の余地を与えないため、プロセスの状態に一貫性が無くなる 可能性があります。緊急事態の時のみ用いてください。

Function: thread-schedule-terminate! thread

[SRFI-226]{gauche.threads} threadを終了させる仕組みを起動します。手続き自体はスレッドの終了を見届けることなく 直ちに返ります。

この手続きはthread-terminate!force引数が行うような「強制的」な 終了も試みません。スレッド終了の最初のステージである、threadに内部的な リクエストを送るところまでをやります。 threadはいずれ終了しますが、それがいつ起きるかについては何も保証はありません。

後でthread-join!を使ってthreadが終了するのを待つことはできます。


9.35.3 スレッドローカル領域

スレッドローカルはスレッドごとの値を格納できるオブジェクトです。 SRFI-226で規定されています。

註: これまで、パラメータがスレッド毎の値を格納するためにも使われてきましたが、 SRFI-226に準拠するために、 動的束縛とスレッドローカル領域を切り離すことにしました。 パラメータは動的束縛のためだけに使われるべきで、スレッド毎の値を保持するには スレッドローカルを使うようにしてください。 既存のコードで、スレッド毎の値を保持するためにパラメータを使っている場合は、 スレッドローカルを使うようにアップデートしてください。 必要な変更についてはSRFI-226パラメータへの移行を参照してください。

Function: make-thread-local init-value :optional inheritable? name

[SRFI-226]{gauche.threads} init-valueを初期値とするあらたなスレッドローカルを作って返します。 省略可能なname引数は、スレッドローカルに名前を与えます。デバッグ時にのみ使われます。

スレッドローカルは一つのScheme値を格納できるスレッド毎の固有領域へのハンドルです。 tlrefでその値を読み出し、tlset!で新たな値をセットできます。 一度スレッドローカルが作られると、それはその時点で存在する、あるいはこれから作られる 全てのスレッドで利用可能になります。

inheritable?引数が#fの場合(デフォルト)、 スレッドローカルが最初にtlrefされると、init-valueが返ります。 一方、inheritable?に真の値を指定すると、 スレッドが作られた時点で、それを作ったスレッドのスレッドローカルの値が初期値として使われます。

Function: thread-local? obj

[SRFI-226]{gauche.threads} objがスレッドローカルなら#tを、そうでなければ#fを返します。

Function: tlref tl

[SRFI-226]{gauche.threads} スレッドローカルtlの呼び出しスレッドでの値を返します。

Function: tlset! tl val

[SRFI-226]{gauche.threads} スレッドローカルtlの呼び出しスレッドでの値をvalに設定します。

Gaucheの拡張として、tlrefset!を使うこともできます:

(set! (tlref tl) obj) ≡ (tlset! tl obj)

9.35.4 同期プリミティブ

mutexと条件変数が低レベルの同期デバイスとして提供されます。 これらはSRFI-18とSRFI-21で定義されているので、サポートしているScheme実装間では ポータブルです。 (Mutex条件変数参照)。

しかし多くの場合、次のような高レベルの同期ユーティリティを使った方が良いです。

アトム

アトムは任意のSchemeオブジェクトをラップして、Javaのsynchronizedブロックに 似た形で同期アクセスを提供します。

セマフォ

決まった数のリソースの利用を振り分けるのに使う、定番の同期プリミティブです。

ラッチ

いくつかの操作が完了するまで、他のスレッドを待たせる同期プリミティブです。

バリア

指定した個数のスレッドが指定ポイントに到達するまで全員で待ち合わせる同期プリミティブです。

MT-Queue

スレッドセーフなキュー(<mtqueue>)がdata.queueモジュールで 提供されます(data.queue - キュー参照)。これは同期チャネルとして動作し、 生産者-消費者パターンを実装するのに適しています。


9.35.4.1 Mutex

Builtin Class: <mutex>

{gauche.threads} 同期のためのプリミティブなデバイスです。次の4つの状態のいずれかを持ちます: locked/owned、locked/not-owned、unlocked/abandoned、unlocked/not-abandoned。 Mutexは、それがunlocked(ロックされていない状態)であるときのみ、 (mutex-lock!により)ロックされます。 所有されている(owned) mutexは、そのmutexを所有しているスレッドを記憶しています。 通常、所有者となるスレッドはmutexをロックしたスレッドですが、 ロックしたのとは別のスレッドがmutexを所有するようにすることもできます。 ロックはmutex-unlock!によるか、所有するスレッドが終了すると解放されます。 前者の場合、mutexはunlocked/not-abandoned(ロックされておらず、放棄されていない状態) になります。 後者の場合、mutexはunlocked/abandoned(ロックされておらず、放棄された状態)になります。

Mutexは、以下の外部スロットを持ちます。

Instance Variable of <mutex>: name

Mutexの名前。

Instance Variable of <mutex>: state

Mutexの状態。これは読み取りのみ可能なスロットです。 下記のmutex-stateの説明を参照して下さい。

Instance Variable of <mutex>: specific

アプリケーションが任意のデータを保持することのできるスロットです。 例えば、アプリケーションはこの固有フィールドで’再帰的な’ mutexを 実装することができます。

Function: mutex? obj

[SRFI-18], [SRFI-21]{gauche.threads} objがmutexであれば#t、そうでなければ#fを返します。

Function: make-mutex :optional name

[SRFI-18], [SRFI-21]{gauche.threads} 新しいmutexオブジェクトを生成して返します。 生成時には、mutexの状態は、unlocked/not-abandoned(ロックされておらず、 放棄されていない状態)です。オプションで、このmutexに名前を付けることができます。

Function: mutex-name mutex

[SRFI-18], [SRFI-21]{gauche.threads} Mutexの名前を返します。

Function: mutex-specific mutex
Function: mutex-specific-set! mutex value

[SRFI-18], [SRFI-21]{gauche.threads} Mutexの固有の値を取得/セットできます。

Function: mutex-state mutex

[SRFI-18], [SRFI-21]{gauche.threads} mutexの状態を返します。状態は以下のうちの1つです。

あるスレッド

Mutexはlocked/owned(ロックされ所有されている)で、所有者は返されたスレッド。

シンボル not-owned

Mutexはlocked/not-owned(ロックされているが所有されていない)。

シンボル abandoned

Mutexはunlocked/abandoned(ロックされておらず、放棄されている)。

シンボル not-abandoned

Mutexはunlocked/not-abandoned(ロックされておらず、放棄されていない)。

Function: mutex-lock! mutex :optional timeout thread

[SRFI-18], [SRFI-21]{gauche.threads} mutexをロックします。mutexがunlocked/not-abandoned( ロックされておらず放棄されていない状態)なら、 この手続きはその状態を排他的なlocked(ロックされた状態)に変更します。 デフォルトでは、mutexはlocked/owned(ロックされ、所有された状態)になり、 所有者は呼び出したスレッドです。 他の所有しているスレッドを、引数threadを与えることもできます。 引数thread#fが与えられると、mutexはlocked/not-owned (ロックされ所有されていない状態)になります。

mutexがunlocked/abandoned(ロックされておらず放棄された状態)ならば、それはつまり、 他の何らかのスレッドがそのロックを解放せずに終了した場合、 この手続きはmutexの状態を変更した後に、’abandoned mutex exception’ (スレッド例外参照)を通知します。

mutexがlocked(ロックされた状態)で、timeoutが省略されるか#fならば、 この手続きはmutexのロックが解放されるまでブロックします。 timeoutが指定されている場合は、ロックが獲得できなかったケースでは 指定された時間に達した時にmutex-lock!は戻ります。 timeoutには、絶対的な時間(<time>オブジェクト、時間参照)か、 相対的な時間を(実数で)指定できます。

mutexのロックが成功するとmutex-lock!#tを返し、 タイムアウトに達すると#fが返ります。

mutexそれ自身は’再帰的なロック’の機能は実装していません。 つまり、mutexをロックしたスレッドが再度mutexをロックしようと すると、そのスレッドはブロックします。しかし、このmutexに 基づいて再帰的なロックのセマンティクスを実装することは難しくありません。 次の例は、SRFI-18のドキュメントから引用したものです。

(define (mutex-lock-recursively! mutex)
  (if (eq? (mutex-state mutex) (current-thread))
      (let ((n (mutex-specific mutex)))
        (mutex-specific-set! mutex (+ n 1)))
      (begin
        (mutex-lock! mutex)
        (mutex-specific-set! mutex 0))))

(define (mutex-unlock-recursively! mutex)
  (let ((n (mutex-specific mutex)))
    (if (= n 0)
        (mutex-unlock! mutex)
        (mutex-specific-set! mutex (- n 1)))))
Function: mutex-unlock! mutex :optional condition-variable timeout

[SRFI-18], [SRFI-21]{gauche.threads} mutexをアンロックします。mutexの状態は、unlocked/not-abandoned (ロックされておらず、放棄されていない状態)となります。 呼び出しているスレッドにより所有されていないmutexをアンロックすることは 許されています。

オプショナル引数のconditional-variableが与えられている場合、 mutex-unlock!は“条件変数待機”の動作も行います(例えば、POSIXスレッドの pthread_cond_wait)。 現在のスレッドはmutexをアンロックし、 condition-variableの待ち状態に入る動作をアトミックに行います。 スレッドは、他のスレッドがcondition-variableにシグナルを通知するか (下記のcondition-variable-signal!condition-variable-broadcast!を 見て下さい)、 timeoutが与えられていてそれに達すると、ブロックが解除されます。 引数timeoutは、絶対的な時間を表す<time>オブジェクト(時間参照)、 相対的な時間を秒数で表す実数、タイムアウトしないことを表す#fのいずれかです。 ブロックが解除された時に、必ずしも条件が満たされているとは限らないので、 次に挙げる例(SRFI-18のドキュメントより引用)のように、 呼び出したスレッドはmutexのロックを再獲得して条件を検査するべきです。

(let loop ()
  (mutex-lock! m)
  (if (condition-is-true?)
      (begin
        (do-something-when-condition-is-true)
        (mutex-unlock! m))
      (begin
        (mutex-unlock! m cv)
        (loop))))

mutex-unlock!の戻り値は、タイムアウトした場合に#f、 それ以外の場合は#tとなります。

Function: mutex-locker mutex
Function: mutex-unlocker mutex

{gauche.threads} それぞれ(lambda () (mutex-lock! mutex))(lambda () (mutex-unlock! mutex)) を返します。 これらのクロージャは、各mutexにつきひとつづつしか作られないため、 タイトなループの中ではこれらの形のリテラルなラムダ式を使うよりも軽量です。

Function: with-locking-mutex mutex thunk

{gauche.threads} mutexをロックしてthunkを呼びます。次のように実装されています。

(define (with-locking-mutex mutex thunk)
  (dynamic-wind
   (mutex-locker mutex)
   thunk
   (mutex-unlocker mutex)))
Macro: run-once expr …

{gauche.threads} 最初にこのフォームが評価された時、expr …が順に評価され、 最後の式の結果(複数であることもあります)が変えされます。 結果は記憶され、以降のこのフォームの評価はexpr …を評価することなく 以前の結果が直ちに返されます。

プロミスをforceするのに似ていると思うかもしれません。 ただ、run-onceは一つのスレッドだけがexprsを評価することを保証します。 複数のスレッドが同じron-onceフォームを同時に評価しようとした場合、 ロックを獲得したスレッドのみがexpr …の評価を行い、 他のスレッドはその結果が出るまで待たされます。 これはforceと異なることに注意してください。 forceは複数のスレッドが同時にプロミスの本体を評価し始めることは禁じておらず、 ただ、最初に確定した結果がプロミスの結果となることのみ保証してまいす。

制御が式の評価を全て完了する前に、エラーなどでフォームから制御が外にに移った場合、 ロックは外され、他のスレッドがexprを評価できるようになります。 何があっても二度評価してはいけない操作がある場合は、guardunwind-protectで適切に保護してください。


9.35.4.2 条件変数

Builtin Class: <condition-variable>

{gauche.threads} 条件変数は、ある条件が真になるのを待っているスレッドの集合を保持します。 あるスレッドがその条件を変更する時、condition-variable-signal!あるいは condition-variable-broadcast!が呼ばれ、それは1つ以上の待機中の スレッドのブロックを解除するため、それらのスレッドは条件が満足するかどうか 検査できます。

条件変数オブジェクトは以下のスロットを持ちます。

Instance Variable of <condition-variable>: name

条件変数の名前。

Instance Variable of <condition-variable>: specific

アプリケーションが任意のデータを保持できるスロット。

SRFI-18は、pthreadのpthread_cond_waitに相当する手続きを 持たないことに注意してください。条件変数を待つのは、 mutex-unlock!の省略可能引数に条件変数を渡し、 その後mutexを再びmutex-lock!で得ることで行います。 この設計は柔軟性のためです。詳しくはSRFI-18を参照して下さい。

このような、pthreadで条件変数を使う定石は:

while (some_condition != TRUE) {
  pthread_cond_wait(condition_variable, mutex);
}

SRFI-18では次のようなコードになります。

(let loop ()
  (unless some-condition
    (mutex-unlock! mutex condition-variable)
    (mutex-lock! mutex)
    (loop)))
Function: condition-variable? obj

[SRFI-18], [SRFI-21]{gauche.threads} objが条件変数なら#t、そうでなければ#fを返します。

Function: make-condition-variable :optional name

[SRFI-18], [SRFI-21]{gauche.threads} 新しい条件変数を返します。オプショナル引数nameで その名前を与えることができます。

Function: condition-variable-name cv

[SRFI-18], [SRFI-21]{gauche.threads} 条件変数の名前を返します。

Function: condition-variable-specific cv
Function: condition-variable-specific-set! cv value

[SRFI-18]、[SRFI-21]{gauche.threads} 条件変数の固有の値を取得/セットします。

Function: condition-variable-signal! cv

[SRFI-18]、[SRFI-21]{gauche.threads} cvで待機しているスレッドがある場合は、それらのうちの1つがスケジューラに より選択され、実行可能にされます。

Function: condition-variable-broadcast! cv

[SRFI-18]、[SRFI-21]{gauche.threads} cvで待機している全てのスレッドのブロックを解除します。


9.35.4.3 アトム

アトムはオブジェクトの集まりをスレッドセーフに簡単に扱うことができるラッパーです。 それぞれの構造に対応するスレッドセーフ版を作るかわりに、 既にあるいくつかのデータをまとめて包んでスレッドセーフにできます。

Function: atom val …

{gauche.threads} val …を初期値に持つアトムオブジェクトを作って返します。

Function: atom? obj

{gauche.threads} objがアトムなら#tを、そうでなければ#fを返します。

以下の手続きは、アトムの中身にアトミックにアクセスしたり、 それをアップデートするのに使えます。共通する省略可能引数として、 timeouttimeout-valを取ります。 これらのデフォルト値は#fです。手続きによっては一つ以上のtimeout-val を取ることがあります。 timeout引数がデフォルト値#fであれば、 以下の手続きはロックが得られるまでブロックします。

timeout引数はすぐにロックが得られなかった場合の動作を変更します。 timeout引数には、絶対的な時刻を指定する<time>オブジェクト (時間参照)か、相対的な時刻を秒数で指定する実数を渡すことができます。 指定された時刻が過ぎたら、以下の手続きはロックを獲得するのを諦めて、 timeout-valに指定された値を返します。 atomicatomic-update!は一つ以上の値を返し得るので、 一つ以上のtimeout-valを渡せます。

Function: atom-ref atom :optional index timeout timeout-val

{gauche.threads} アトムatomindex番目の値を返します。 timeouttimeout-valについては上記参照。

(define a (atom 'a 'b))

(atom-ref a 0) ⇒ a
(atom-ref a 1) ⇒ b
Function: atomic atom proc :optional timeout timeout-val timeout-val2 …

{gauche.threads} atomをロックして、procを現在のatomの値に適用します。 procatomの値の数だけの引数を取ることができなければなりません。

タイムアウトが起こらない限り、procが返した値がatomicの返り値となります。 timeouttimeout-valについては上記参照。

例えば次のref/countは、 ハッシュテーブルが参照された回数を数えるスレッドセーフな手続きです。

(define a (atom (make-hash-table 'eq?) (list 0)))

(define (ref/count a key)
  (atomic a
   (lambda (ht count-cell)
     (inc! (car count-cell))
     (hash-table-get h key))))
Function: atomic-update! atom proc :optional timeout timeout-val timeout-val2 …

{gauche.threads} atomをロックして、その値を引数としてprocを呼び、 procの戻り値をatomの新たな値とします。 procatomの値の数だけ引数を取らねばならず、 またatomの値の数以上の戻り値を返さねばなりません。 procatomの値の数以上の値を返した場合、 余分な値はatomを更新するのには使われませんが、 atomic-update!の結果には含まれます。

atomic-update!の戻り値は、タイムアウトが起こらなければprocの戻り値です。 timeouttimeout-valについては上記参照。

下はスレッドセーフなカウンタです。

(define a (atom 0))

(atomic-update! a (cut + 1 <>))

atomは古のLispでは「コンスセル(ペア)でないもの」を指していました。 当時、コンスセルは唯一の複合型であり、他の型もごくわずか(数値とシンボル)だったので、 セルでないものを指すことには意味がありました。

今でもatomはLispのチュートリアルで目にすることがありますが、 Schemeをはじめ現代のLispではたくさんのデータ型があり、 コンスセル以外をひとまとめにする用語を使う意味がほとんどありません。

Clojureはatomをスレッドセーフ(アトミック)な基本データに使ったので、 Gaucheもそれに倣うことにしました。

註: コンストラクタがmake-atomではなくatomなのは、 list/make-listvector/make-vectorstring/make-stringなどの慣習に従っています。 つまり、make-のない手続きは、各要素を可変長引数の形で取ります。


9.35.4.4 セマフォ

Builtin Class: <semaphore>

{gauche.threads} セマフォは、決まった数のリソースを使うための「トークン」を管理します。 リソースを使いたいスレッドはsemaphore-acquire!を呼びトークンを獲得します。 リソースを使い終わったらsemaphore-release!でトークンを返します。 スレッドがトークンを要求した時点でトークンが余っていなければ、他のスレッドがトークンを返す まで待ちます。

Function: make-semaphore :optional init-value name

{gauche.threads} 新たなセマフォを作って返します。init-valueは初期段階で使えるトークンの数を 指定します。省略された場合は0になります。 もう一つの省略可能引数nameは任意のSchemeオブジェクトで、 セマフォの表示にのみ使われます。

Function: semaphore? obj

{gauche.threads} objがセマフォなら#tを、そうでなければ#fを返します。

Function: semaphore-acquire! sem :optional timeout timeout-val

{gauche.threads} セマフォsemからトークンを獲得します。トークンがあれば、 トークンの残り数をデクリメントして、直ちに#tを返します。 もしトークンが余ってなければ、新たなトークンが返却されるか、 timeoutが指定するタイムアウトまで待ちます。 タイムアウトした場合はtimeout-valが返されます。timeout-valの デフォルトは#fです。

timeoutに渡せる値はmutexと同じで、 #f (タイムアウトなし。デフォルト値)、 絶対的な時刻を指定する<time>オブジェクト、 あるいは現在からの秒数を相対的に指定する実数です。

Function: semaphore-release! sem :optional count

{gauche.threads} トークンをセマフォsemに返却します。semのトークン数をインクリメントするのと 同時に、semaphore-acquire!で待っているスレッドのひとつを走らせます。

count引数で返却するトークンの数を指定できます。デフォルトは1です。

実のところ、「トークン」と読んでいるのはセマフォのモデルを理解しやすくするためで、 内部的には単なるカウンタにすぎません。なので semaphore-acuqire!を呼んでいなくてもsemaphore-release!でトークンを追加 することができます。


9.35.4.5 ラッチ

Builtin Class: <latch>

{gauche.threads} ラッチはカウンタを内部に持つ同期デバイスです。 カウンタが0になるまで、スレッドはいくつでも待ちます。 カウンタが0になった途端、全てのスレッドが走ります。 (セマフォでは、内部のカウンタは待つスレッドの数にも影響しますが、 ラッチではカウンタと待つスレッドの数は無関係です)。

最も単純な用途では、初期カウントを1から始めます。これはしばしば「ゲート」と呼ばれます。 待つスレッドはゲートに達したらそこで停止し、ひとたびカウンタをデクリメントしたら、 全てのスレッドがそこから走り始めます。

Function: make-latch initial-count :optional name

{gauche.threads} カウンタの初期値としてinitial-countを持つラッチを作って返します。 initial-countは正確な正整数でなければなりません。

省略可能なname引数は任意のSchemeオブジェクトで、 ラッチを表示する時にのみ使われます。

Function: latch? obj

{gauche.threads} objがラッチなら#tを、そうでなければ#fを返します。

Function: latch-dec! latch :optional n

{gauche.threads} ラッチのカウンタをnだけ減らします。nが省略されたら1を使います。

カウンタの値が0以下になったら、ラッチで待っているスレッドを起こします。

更新後のカウンタの値を返します。

nは正確な整数でなければなりません。0や負の値も許されます。

Function: latch-clear! latch

{gauche.threads} latchのカウンタが0でなければ、それを0にし、待っているスレッドを起こします。 既に0であれば何もしません。

0にする前のカウンタの値を返します。

Function: latch-await latch :optional timeout timeout-val

{gauche.threads} ラッチlatchのカウンタが0以下なら直ちに#tを返します。 そうでなければ、呼び出しスレッドはカウンタが0以下になるか、タイムアウトするまで ブロックされます。カウンタによりスレッドが再開された場合は#tが、 タイムアウトした場合はtimeout-valが返されます。 timeout-valのデフォルトは#fです。

timeout引数は、 #f (タイムアウトなし。デフォルト値)、 絶対的な時刻を指定する<time>オブジェクト、 あるいは現在からの秒数を相対的に指定する実数です。


9.35.4.6 バリア

Builtin Class: <barrier>

{gauche.threads} バリアは、指定された個数のスレッドが待ち状態になるまで待ち合わせる 同期プリミティブです。 barrier-awaitを呼んだスレッドが指定の個数に達したら、 全てのスレッドが再開し、バリアは初期状態に戻ります。 次にバリアに到達したスレッドは再び待ちに入ります。

バリアは「アクション」を持つことができます。アクションは、待ち合わせスレッドが しきい値に到達した時に、他のスレッドが走り出す前に実行されます。

待っているスレッドのいずれかがタイムアウトするか、アクションで捕捉されない例外が 投げられた場合、バリアは「破れた」状態になります。 バリアが壊れると、待っていたスレッドはあたかもタイムアウトが起きたかのように 再開されます。破れた状態はbarrier-reset!で明示的にリセットされるまで 保たれ、その間にバリアに到達したスレッドはあたかも0のタイムアウトが指定されたかのように そのまま素通りします。

Function: make-barrier threshold :optional action name

{gauche.threads} 新しいバリアを作って返します。thresholdは正確な非負整数でなければならず、 待ち合わせるスレッドの数を指定します。barrier-awaitを 呼んだスレッドがこの数に達した時に、それら全てのスレッドで barrier-awaitから制御が戻ります。

省略可能なaction引数は#f(アクションなし)かサンクでなければなりません。 サンクの場合、それはバリアが開放される前の最後にbarrier-awaitを呼んだ スレッドによって、他のスレッドが走り始める前に実行されます。

name引数は任意のSchemeオブジェクトで、バリアを表示する際にのみ使われます。

Function: barrier? obj

{gauche.threads} objがバリアなら#tを、そうでなければ#fを返します。

Function: barrier-await barrier :optional timeout timeout-val

{gauche.threads} barrierで待っているスレッドが閾値に達するか、バリアが破れるまで、 呼び出したスレッドを待たせます。

バリアが破れるのは、待っているスレッドのどれかがタイムアウトするか、 バリアのアクションから例外が投げられた時です。

timeouttimeout-val引数は他の同期プリミティブと同じです。 timeout#t (タイムアウトなし、デフォルト)か、 絶対的時刻を指定する<time>オブジェクトか、 相対的な秒数を指定する実数です。 timeout-valbarrier-awaitがタイムアウトかバリア破れで 戻る場合に返される値で、デフォルトは#fです。

この手続きの戻り値は、待ちスレッド数が閾値に達して戻った場合は正確な整数、 バリアが破れた場合はtimeout-valです。 正確な整数は、barrier-awaitを呼び出した時点であといくつ待ちスレッドが 必要だったかを示します。つまり、最初にバリアに到達したスレッドなら threshold - 1、 最後に到達したなら0です。

barrier-awaitを呼んだ時点で既にバリアが破れていたら、 直ちにtimeout-valが返されます。

スレッド数が閾値に達して全スレッドを解放した場合は、 バリアは初期状態に戻ります。つまり、次にbarrier-awaitを呼んだスレッドは 再び待たされることになります。 バリアが破れたば場合、barrier-reset!が呼ばれるまでバリアはずっと破れた 状態のままになります。

Function: barrier-broken? barrier

{gauche.threads} バリアbarrierが破れた状態なら#tを、そうでなければ#fを返します。

Function: barrier-reset! barrier

{gauche.threads} バリアを初期状態にリセットします。バリアが破れた状態なら、破れていない状態に戻ります。 バリアで待っているスレッドがいた場合、それはあたかもスレッド数が閾値に達したかのように 解放されます。


9.35.5 スレッド例外

例外のいくつかのタイプは、スレッド関連の手続きから投げられます。 これらの例外は、Gaucheの例外メカニズム(例外参照)により 扱われます。

これらは元々SRFI-18で定義されました。SRFI-226もそれに倣いましたが、 名前については現代的なSchemeの名前付けを意識したものに変えています。 Gaucheではどちらの名前もサポートします。

Builtin Class: <thread-exception>
Condition Type: &thread

[SRFI-226]{gauche.threads} スレッド関連の例外の基底クラスです。<exception>クラスを継承しています。 スロットを1つ持っています。

Instance Variable of <thread-exception>: thread

この例外を投げたスレッド。

&threadというコンディション名はSRFI-226での定義です。 ただし、SRFI-226のコンディションはthreadスロットは規定していません。

Builtin Class: <join-timeout-exception>
Condition Type: &thread-timeout

[SRFI-226]{gauche.threads} 待機していたスレッドが戻る前にタイムアウトに達した時にthread-join!によって 投げられる例外。<thread-exception>を継承しています。

&thread-timeoutという名前はSRFI-226で定義されています。

Builtin Class: <abandoned-mutex-exception>
Condition Type: &thread-abandoned-mutex

[SRFI-226]{gauche.threads} ロックされるmutexが、unlocked/abandoned(ロックされておらず、放棄された状態) であるときにmutex-lock!により投げられる例外。 <thread-exception>を継承しています。スロットを1つ持ちます。

Instance Variable of <abandoned-mutex-exception>: mutex

この例外の原因となったmutex。

&thread-abandoned-mutexという名前はSRFI-226で定義されています。

Builtin Class: <terminated-thread-exception>
Condition Type: &thread-already-terminated

[SRFI-226]{gauche.threads} 待機していたスレッドが(thread-terminate!により)異常終了した 場合に(thread-join!により)投げられる例外。 <thread-exception>を継承し、スロットを1つ持ちます。

Instance Variable of <terminated-thread-exception>: terminator

この例外の原因となったスレッドを終了したスレッド。

&thread-abandoned-mutexという名前はSRFI-226で定義されています。 ただし、SRFI-226はterminatorスロットは定義していません。

Builtin Class: <uncaught-exception>
Condition Type: &uncaught-exception

[SRFI-226]{gauche.threads} 待機していたスレッドが捕捉されない例外により終了された場合に thread-join!により投げられる例外。 <thread-exception>を継承し、スロットを1つ持ちます。

Instance Variable of <uncaught-exception>: reason

そのスレッドの終了の原因となった例外。

&uncaught-exceptionという名前はSRFI-226で定義されています。

Builtin Class: <concurrent-modification-violation>
Condition Type: &concurrent-modification

[SRFI-226] これはスレッドセーフでないデータを複数スレッドで同時に変更しようとした場合に 投げられるコンディションです。SRFI-226で定義されていますが、 SRFIはこの状況を検出するのにオーバヘッドがある場合は検出しなくても良いとしています。 今のところ、GaucheのAPIでこのコンディションを検出するものはありません。 将来はできるかもしれません。今はこれはSRFI-226との互換性のためのみで提供されています。

Function: make-thread-condition

[SRFI-226]{gauche.threads} 新しい<thread-exception>コンディションを作ります。 これはSRFI-226との互換のために用意されていますが、直接使う必要はほとんどないでしょう。 実際には<thread-exception>のサブクラスのコンディションが使われるからです。 コンディションのthreadスロットは呼び出したスレッドで初期化されます。

Function: thread-condition? obj

[SRFI-226]{gauche.threads} obj<thread-exception> (&thread)コンディションなら#tを、 そうでなければ#fを返します。

Function: make-thread-timeout-condition

[SRFI-226]{gauche.threads} 新たな<join-timeout-exception> (&thread-timeout)コンディション を作ります。

Function: join-timeout-exception? obj
Function: thread-timeout-condition? obj

[SRFI-18], [SRFI-21], [SRFI-226]{gauche.threads} obj<join-timeout-exception> (&thread-timeout) コンディションなら#tを、 そうでなければ#fを返します。 join-timeout-exception?はSRFI-18/21で、 thread-timeout-condition?はSRFI-226で定義された名前です。どちらも同じです。

Function: make-thread-abandoned-mutex

[SRFI-226]{gauche.threads} 新たな<abandoned-mutex-exception> (&thread-abandoned-mutex)コンディションを作ります。

Function: abandoned-mutex-exception? obj
Function: thread-abandoned-mutex-condition? obj

[SRFI-18], [SRFI-21], [SRFI-226]{gauche.threads} obj<abandoned-mutex-exception>のインスタンスなら#tを、 そうでなければ#fを返します。 abandoned-mutex-exception?はSRFI-18/21で、 thread-abandoned-mutex-condition?はSRFI-226で定義された名前です。どちらも同じです。

Function: make-thread-already-terminated-condition

[SRFI-226]{gauche.threads} 新たな<termianted-thread-exception> (&thread-already-terminated)コンディションを作ります。

Function: terminated-thread-exception? obj
Function: thread-already-terminated-condition? obj

[SRFI-18], [SRFI-21], [SRFI-226]{gauche.threads} obj<terminated-thread-exception>のインスタンスなら#tを、 そうでなければ#fを返します。 terminated-thread-exception?はSRFI-18/21で、 thread-already-termianted-condition?はSRFI-226で定義された名前です。どちらも同じです。

Function: make-uncaught-exception-condition reason

[SRFI-226]{gauche.threads} 新たな<uncaught-exception> (&uncaught-exception)コンディションを作ります。 引数はコンディションのreasonスロットに保持されます。

Function: uncaught-exception? obj
Function: uncaught-exception-condition? obj

[SRFI-18], [SRFI-21], [SRFI-226]{gauche.threads} obj<uncaught-exception>のインスタンスなら#tを、 そうでなければ#fを返します。 uncaught-exception?はSRFI-18/21で、 uncaught-exception-condition?はSRFI-226で定義された名前です。どちらも同じです。

Function: uncaught-exception-reason exc
Function: uncaught-exception-condition-reason exc

[SRFI-18], [SRFI-21], [SRFI-226]{gauche.threads} <uncaught-exception>オブジェクトのreasonスロットの値を 返します。

uncaught-exception-reasonはSRFI-18/21で、 uncaught-exception-condition-reasonはSRFI-226で定義された名前です。 どちらも同じ動作です。

Function: make-concurrent-modification-violation

[SRFI-226]{gauche.threads} 新たな<concurrent-modification-violation> (&concurrent-modification)コンディションを作ります。

Function: concurrent-modification-violation? obj

[SRFI-226]{gauche.threads} obj<concurrent-modification-violation (&concurrent-modification)コンディションなら#tを、 そうでなければ#fを返します。



For Development HEAD DRAFTSearch (procedure/syntax/module):
DRAFT