gauche.threads
- スレッド ¶Gaucheでは、POSIXスレッド(pthreads)か Windowsスレッド上に構築されるスレッドを使うことができます (どちらを使うかはプラットフォームによります)。
スレッドを扱うAPIを提供します。スレッドサポートは組み込みで、 このモジュールは名前空間を提供しているだけです。
Schemeレベルでは、スレッドサブシステムの違い(pthreadsかWindowsスレッドか)を
意識する必要はほどんど無いでしょう。ただ、特定のサブシステムのセマンティクスに
依存したコードを書きたい場合は、
pthreadプラットフォームとWindowsスレッドプラットフォームで
それぞれ、gauche.sys.pthreads
とgauche.sys.wthreads
という機能識別子が用意されています。
cond-expand
でプラットフォーム依存のコードを切り替えられます
(機能条件式参照)。
コンパイル時ではなく実行時にスレッドが有効かどうかをチェックするためには、 次の手続きを使います。
{gauche.threads
}
サポートされているスレッドのタイプを表すシンボルを返します。
POSIX pthreadsならpthread
、
Windowsスレッドならwthread
が返されます。
(註:pthreadプラットフォームではシンボルpthread
ではなく
pthreads
を返すべきでした。そうすれば、
gauche-thread-type
の返り値はコンフィグレーション時に
--enable-threads
に与えた値と対応するものになっていたでしょう。
互換性のために、残念ながらこの見過ごしは修正されないでしょう。
SchemeレベルのスレッドAPIはSRFI-18、“マルチスレッドサポート”を満たし、 Gaucheのオブジェクトのインターフェースでラップされます。
• スレッドプログラミングTips: | ||
• スレッド手続き: | ||
• スレッドローカル領域: | ||
• 同期プリミティブ: | ||
• スレッド例外: |
スレッドの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参照)。
call/cc
による協調スレッド
テクニックが使えるでしょう。call/ccベースのスレッドの生成は、
ネイティブスレッドを生成するよりも高速です。
select
ベースのディスパッチ(gauche.selector
- 簡単なディスパッチャに
例があります)を使うほうが簡単なことがあります。
もちろん、これらのテクニックはネイティブスレッドとは相互排他ではありません。 例えば、“スレッドプール”テクニックと一緒にディスパッチャを使うこともできます。 それらの機能を実現するために、ネイティブスレッドが唯一の方法ではないということを 心に留め置いて下さい。
シングルスレッドのプログラムが予期せぬ(捕捉されない)エラーを起こした場合、 デフォルトでは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)))))
スレッドを表すクラスです。それぞれのスレッドは、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つの読み込みプリミティブ(例えば、read
やread-char
、
read-line
など)がアトミックに実行されます。
シグナルハンドラは全てのスレッドで共有されますが、それぞれのスレッドは 独自のシグナルマスクを持ちます。詳細は、シグナルとスレッドを参照。
スレッドオブジェクトは以下の外部スロットを持ちます。
<thread>
: name ¶スレッドに関連付けられる名前。
これは単にアプリケーションにとっての便宜を図るためのものです。
原始となるスレッドは“root
”という名前を持ちます。
<thread>
: specific ¶アプリケーションが使うスレッドローカルなスロット。
[SRFI-18], [SRFI-21]{gauche.threads
}
現在のスレッドを返します。
[SRFI-18], [SRFI-21]{gauche.threads
}
objがスレッドなら#t
、そうでなければ#f
を返します。
[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!
が呼ばれるまで生成されません。
{gauche.threads
}
threadの状態を示す、new
、runnable
、stopped
、
terminated
のいずれかのシンボルを返します。
[SRFI-18], [SRFI-21]{gauche.threads
}
threadのスロットnameの値を返します。
[SRFI-18], [SRFI-21]{gauche.threads
}
threadの指定したスレッドの値を取得/設定します。
[SRFI-18], [SRFI-21]{gauche.threads
}
threadを開始します。threadが “new” 状態でなければエラーが投げられます。
threadを返します。
{gauche.threads
}
threadが “new” 状態であればそれを開始し、thread自身を返します。
そうでなければ#f
を返します。
スレッドは開始する前に “terminated” 状態になる場合があります。他のスレッドが
そのスレッドに対してthread-terminate!
を呼んだ場合です。
開始する前に終了してしまったスレッドに対してthread-start!
を呼ぶとエラーが
投げられますが、この手続きではエラーになりません。
[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>
を投げます。
これらの例外の詳細についてはスレッド例外を参照してください。
[SRFI-18], [SRFI-21]{gauche.threads
}
呼び出しているスレッドの実行を中断し、他に待機中の実行可能なスレッドがあれば、
CPUにそれを処理させます。
[SRFI-18], [SRFI-21]{gauche.threads
}
呼び出しているスレッドをtimeoutに指定した時間だけ中断します。
timeoutは絶対的な時間を表す<time>
オブジェクト(時間参照)か、
この手続きが呼ばれた時刻からの相対的な秒数を表す実数でなければなりません。
指定された時間が経過すると、thread-sleep!
は未定義値を返します。
timeoutが過去の時間を指していたら、thread-sleep!
はすぐに戻ります。
{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
でもない状態の場合は
エラーが報告されます。
{gauche.threads
}
thread-stop!
で停止したthreadの実行を再開します。
threadが停止状態でなかったり、thread-cont!
を呼び出したのとは
別のスレッドによって停止させられていた場合はエラーが報告されます。
停止要求を既に出していて、タイムアウトしていた場合、thread-cont!
は
その停止要求をキャンセルする役割を持ちます。
[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_cancel
やTerminateThread
コールによって
行われ、スレッドに後始末の余地を与えないため、プロセスの状態に一貫性が無くなる
可能性があります。緊急事態の時のみ用いてください。
[SRFI-226]{gauche.threads
}
threadを終了させる仕組みを起動します。手続き自体はスレッドの終了を見届けることなく
直ちに返ります。
この手続きはthread-terminate!
のforce引数が行うような「強制的」な
終了も試みません。スレッド終了の最初のステージである、threadに内部的な
リクエストを送るところまでをやります。
threadはいずれ終了しますが、それがいつ起きるかについては何も保証はありません。
後でthread-join!
を使ってthreadが終了するのを待つことはできます。
スレッドローカルはスレッドごとの値を格納できるオブジェクトです。 SRFI-226で規定されています。
註: これまで、パラメータがスレッド毎の値を格納するためにも使われてきましたが、 SRFI-226に準拠するために、 動的束縛とスレッドローカル領域を切り離すことにしました。 パラメータは動的束縛のためだけに使われるべきで、スレッド毎の値を保持するには スレッドローカルを使うようにしてください。 既存のコードで、スレッド毎の値を保持するためにパラメータを使っている場合は、 スレッドローカルを使うようにアップデートしてください。 必要な変更についてはSRFI-226パラメータへの移行を参照してください。
[SRFI-226]{gauche.threads
}
init-valueを初期値とするあらたなスレッドローカルを作って返します。
省略可能なname引数は、スレッドローカルに名前を与えます。デバッグ時にのみ使われます。
スレッドローカルは一つのScheme値を格納できるスレッド毎の固有領域へのハンドルです。
tlref
でその値を読み出し、tlset!
で新たな値をセットできます。
一度スレッドローカルが作られると、それはその時点で存在する、あるいはこれから作られる
全てのスレッドで利用可能になります。
inheritable?引数が#f
の場合(デフォルト)、
スレッドローカルが最初にtlref
されると、init-value
が返ります。
一方、inheritable?に真の値を指定すると、
スレッドが作られた時点で、それを作ったスレッドのスレッドローカルの値が初期値として使われます。
[SRFI-226]{gauche.threads
}
objがスレッドローカルなら#t
を、そうでなければ#f
を返します。
[SRFI-226]{gauche.threads
}
スレッドローカルtlの呼び出しスレッドでの値を返します。
[SRFI-226]{gauche.threads
}
スレッドローカルtlの呼び出しスレッドでの値をvalに設定します。
Gaucheの拡張として、tlref
とset!
を使うこともできます:
(set! (tlref tl) obj) ≡ (tlset! tl obj)
mutexと条件変数が低レベルの同期デバイスとして提供されます。 これらはSRFI-18とSRFI-21で定義されているので、サポートしているScheme実装間では ポータブルです。 (Mutex、条件変数参照)。
しかし多くの場合、次のような高レベルの同期ユーティリティを使った方が良いです。
アトムは任意のSchemeオブジェクトをラップして、Javaのsynchronized
ブロックに
似た形で同期アクセスを提供します。
決まった数のリソースの利用を振り分けるのに使う、定番の同期プリミティブです。
いくつかの操作が完了するまで、他のスレッドを待たせる同期プリミティブです。
指定した個数のスレッドが指定ポイントに到達するまで全員で待ち合わせる同期プリミティブです。
スレッドセーフなキュー(<mtqueue>
)がdata.queue
モジュールで
提供されます(data.queue
- キュー参照)。これは同期チャネルとして動作し、
生産者-消費者パターンを実装するのに適しています。
• 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は、以下の外部スロットを持ちます。
<mutex>
: name ¶Mutexの名前。
<mutex>
: state ¶Mutexの状態。これは読み取りのみ可能なスロットです。
下記のmutex-state
の説明を参照して下さい。
<mutex>
: specific ¶アプリケーションが任意のデータを保持することのできるスロットです。 例えば、アプリケーションはこの固有フィールドで’再帰的な’ mutexを 実装することができます。
[SRFI-18], [SRFI-21]{gauche.threads
}
objがmutexであれば#t
、そうでなければ#f
を返します。
[SRFI-18], [SRFI-21]{gauche.threads
}
新しいmutexオブジェクトを生成して返します。
生成時には、mutexの状態は、unlocked/not-abandoned(ロックされておらず、
放棄されていない状態)です。オプションで、このmutexに名前を付けることができます。
[SRFI-18], [SRFI-21]{gauche.threads
}
Mutexの名前を返します。
[SRFI-18], [SRFI-21]{gauche.threads
}
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(ロックされておらず、放棄されていない)。
[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)))))
[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
となります。
{gauche.threads
}
それぞれ(lambda () (mutex-lock! mutex))
と
(lambda () (mutex-unlock! mutex))
を返します。
これらのクロージャは、各mutexにつきひとつづつしか作られないため、
タイトなループの中ではこれらの形のリテラルなラムダ式を使うよりも軽量です。
{gauche.threads
}
mutexをロックしてthunkを呼びます。次のように実装されています。
(define (with-locking-mutex mutex thunk) (dynamic-wind (mutex-locker mutex) thunk (mutex-unlocker mutex)))
{gauche.threads
}
最初にこのフォームが評価された時、expr …が順に評価され、
最後の式の結果(複数であることもあります)が変えされます。
結果は記憶され、以降のこのフォームの評価はexpr …を評価することなく
以前の結果が直ちに返されます。
プロミスをforce
するのに似ていると思うかもしれません。
ただ、run-once
は一つのスレッドだけがexprsを評価することを保証します。
複数のスレッドが同じron-once
フォームを同時に評価しようとした場合、
ロックを獲得したスレッドのみがexpr …の評価を行い、
他のスレッドはその結果が出るまで待たされます。
これはforce
と異なることに注意してください。
force
は複数のスレッドが同時にプロミスの本体を評価し始めることは禁じておらず、
ただ、最初に確定した結果がプロミスの結果となることのみ保証してまいす。
制御が式の評価を全て完了する前に、エラーなどでフォームから制御が外にに移った場合、
ロックは外され、他のスレッドがexprを評価できるようになります。
何があっても二度評価してはいけない操作がある場合は、guard
や
unwind-protect
で適切に保護してください。
{gauche.threads
}
条件変数は、ある条件が真になるのを待っているスレッドの集合を保持します。
あるスレッドがその条件を変更する時、condition-variable-signal!
あるいは
condition-variable-broadcast!
が呼ばれ、それは1つ以上の待機中の
スレッドのブロックを解除するため、それらのスレッドは条件が満足するかどうか
検査できます。
条件変数オブジェクトは以下のスロットを持ちます。
<condition-variable>
: name ¶条件変数の名前。
<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)))
[SRFI-18], [SRFI-21]{gauche.threads
}
objが条件変数なら#t
、そうでなければ#f
を返します。
[SRFI-18], [SRFI-21]{gauche.threads
}
新しい条件変数を返します。オプショナル引数nameで
その名前を与えることができます。
[SRFI-18], [SRFI-21]{gauche.threads
}
条件変数の名前を返します。
[SRFI-18]、[SRFI-21]{gauche.threads
}
条件変数の固有の値を取得/セットします。
[SRFI-18]、[SRFI-21]{gauche.threads
}
cvで待機しているスレッドがある場合は、それらのうちの1つがスケジューラに
より選択され、実行可能にされます。
[SRFI-18]、[SRFI-21]{gauche.threads
}
cvで待機している全てのスレッドのブロックを解除します。
アトムはオブジェクトの集まりをスレッドセーフに簡単に扱うことができるラッパーです。 それぞれの構造に対応するスレッドセーフ版を作るかわりに、 既にあるいくつかのデータをまとめて包んでスレッドセーフにできます。
{gauche.threads
}
val …を初期値に持つアトムオブジェクトを作って返します。
{gauche.threads
}
objがアトムなら#t
を、そうでなければ#f
を返します。
以下の手続きは、アトムの中身にアトミックにアクセスしたり、
それをアップデートするのに使えます。共通する省略可能引数として、
timeoutとtimeout-valを取ります。
これらのデフォルト値は#f
です。手続きによっては一つ以上のtimeout-val
を取ることがあります。
timeout引数がデフォルト値#f
であれば、
以下の手続きはロックが得られるまでブロックします。
timeout引数はすぐにロックが得られなかった場合の動作を変更します。
timeout引数には、絶対的な時刻を指定する<time>
オブジェクト
(時間参照)か、相対的な時刻を秒数で指定する実数を渡すことができます。
指定された時刻が過ぎたら、以下の手続きはロックを獲得するのを諦めて、
timeout-valに指定された値を返します。
atomic
とatomic-update!
は一つ以上の値を返し得るので、
一つ以上のtimeout-valを渡せます。
{gauche.threads
}
アトムatomのindex番目の値を返します。
timeoutとtimeout-valについては上記参照。
(define a (atom 'a 'b)) (atom-ref a 0) ⇒ a (atom-ref a 1) ⇒ b
{gauche.threads
}
atomをロックして、procを現在のatomの値に適用します。
procはatomの値の数だけの引数を取ることができなければなりません。
タイムアウトが起こらない限り、procが返した値がatomic
の返り値となります。
timeoutとtimeout-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))))
{gauche.threads
}
atomをロックして、その値を引数としてprocを呼び、
procの戻り値をatomの新たな値とします。
procはatomの値の数だけ引数を取らねばならず、
またatomの値の数以上の戻り値を返さねばなりません。
procがatomの値の数以上の値を返した場合、
余分な値はatomを更新するのには使われませんが、
atomic-update!
の結果には含まれます。
atomic-update!
の戻り値は、タイムアウトが起こらなければprocの戻り値です。
timeoutとtimeout-valについては上記参照。
下はスレッドセーフなカウンタです。
(define a (atom 0)) (atomic-update! a (cut + 1 <>))
atomは古のLispでは「コンスセル(ペア)でないもの」を指していました。 当時、コンスセルは唯一の複合型であり、他の型もごくわずか(数値とシンボル)だったので、 セルでないものを指すことには意味がありました。
今でもatomはLispのチュートリアルで目にすることがありますが、 Schemeをはじめ現代のLispではたくさんのデータ型があり、 コンスセル以外をひとまとめにする用語を使う意味がほとんどありません。
Clojureはatomをスレッドセーフ(アトミック)な基本データに使ったので、 Gaucheもそれに倣うことにしました。
註: コンストラクタがmake-atom
ではなくatom
なのは、
list
/make-list
、vector
/make-vector
、
string
/make-string
などの慣習に従っています。
つまり、make-
のない手続きは、各要素を可変長引数の形で取ります。
{gauche.threads
}
セマフォは、決まった数のリソースを使うための「トークン」を管理します。
リソースを使いたいスレッドはsemaphore-acquire!
を呼びトークンを獲得します。
リソースを使い終わったらsemaphore-release!
でトークンを返します。
スレッドがトークンを要求した時点でトークンが余っていなければ、他のスレッドがトークンを返す
まで待ちます。
{gauche.threads
}
新たなセマフォを作って返します。init-valueは初期段階で使えるトークンの数を
指定します。省略された場合は0になります。
もう一つの省略可能引数nameは任意のSchemeオブジェクトで、
セマフォの表示にのみ使われます。
{gauche.threads
}
objがセマフォなら#t
を、そうでなければ#f
を返します。
{gauche.threads
}
セマフォsemからトークンを獲得します。トークンがあれば、
トークンの残り数をデクリメントして、直ちに#t
を返します。
もしトークンが余ってなければ、新たなトークンが返却されるか、
timeoutが指定するタイムアウトまで待ちます。
タイムアウトした場合はtimeout-valが返されます。timeout-valの
デフォルトは#f
です。
timeoutに渡せる値はmutexと同じで、
#f
(タイムアウトなし。デフォルト値)、
絶対的な時刻を指定する<time>
オブジェクト、
あるいは現在からの秒数を相対的に指定する実数です。
{gauche.threads
}
トークンをセマフォsemに返却します。semのトークン数をインクリメントするのと
同時に、semaphore-acquire!で待っているスレッドのひとつを走らせます。
count引数で返却するトークンの数を指定できます。デフォルトは1です。
実のところ、「トークン」と読んでいるのはセマフォのモデルを理解しやすくするためで、
内部的には単なるカウンタにすぎません。なので
semaphore-acuqire!
を呼んでいなくてもsemaphore-release!
でトークンを追加
することができます。
{gauche.threads
}
ラッチはカウンタを内部に持つ同期デバイスです。
カウンタが0になるまで、スレッドはいくつでも待ちます。
カウンタが0になった途端、全てのスレッドが走ります。
(セマフォでは、内部のカウンタは待つスレッドの数にも影響しますが、
ラッチではカウンタと待つスレッドの数は無関係です)。
最も単純な用途では、初期カウントを1から始めます。これはしばしば「ゲート」と呼ばれます。 待つスレッドはゲートに達したらそこで停止し、ひとたびカウンタをデクリメントしたら、 全てのスレッドがそこから走り始めます。
{gauche.threads
}
カウンタの初期値としてinitial-countを持つラッチを作って返します。
initial-countは正確な正整数でなければなりません。
省略可能なname引数は任意のSchemeオブジェクトで、 ラッチを表示する時にのみ使われます。
{gauche.threads
}
objがラッチなら#t
を、そうでなければ#f
を返します。
{gauche.threads
}
ラッチのカウンタをnだけ減らします。nが省略されたら1を使います。
カウンタの値が0以下になったら、ラッチで待っているスレッドを起こします。
更新後のカウンタの値を返します。
nは正確な整数でなければなりません。0や負の値も許されます。
{gauche.threads
}
latchのカウンタが0でなければ、それを0にし、待っているスレッドを起こします。
既に0であれば何もしません。
0にする前のカウンタの値を返します。
{gauche.threads
}
ラッチlatchのカウンタが0以下なら直ちに#t
を返します。
そうでなければ、呼び出しスレッドはカウンタが0以下になるか、タイムアウトするまで
ブロックされます。カウンタによりスレッドが再開された場合は#t
が、
タイムアウトした場合はtimeout-valが返されます。
timeout-valのデフォルトは#f
です。
timeout引数は、
#f
(タイムアウトなし。デフォルト値)、
絶対的な時刻を指定する<time>
オブジェクト、
あるいは現在からの秒数を相対的に指定する実数です。
{gauche.threads
}
バリアは、指定された個数のスレッドが待ち状態になるまで待ち合わせる
同期プリミティブです。
barrier-await
を呼んだスレッドが指定の個数に達したら、
全てのスレッドが再開し、バリアは初期状態に戻ります。
次にバリアに到達したスレッドは再び待ちに入ります。
バリアは「アクション」を持つことができます。アクションは、待ち合わせスレッドが しきい値に到達した時に、他のスレッドが走り出す前に実行されます。
待っているスレッドのいずれかがタイムアウトするか、アクションで捕捉されない例外が
投げられた場合、バリアは「破れた」状態になります。
バリアが壊れると、待っていたスレッドはあたかもタイムアウトが起きたかのように
再開されます。破れた状態はbarrier-reset!
で明示的にリセットされるまで
保たれ、その間にバリアに到達したスレッドはあたかも0のタイムアウトが指定されたかのように
そのまま素通りします。
{gauche.threads
}
新しいバリアを作って返します。thresholdは正確な非負整数でなければならず、
待ち合わせるスレッドの数を指定します。barrier-await
を
呼んだスレッドがこの数に達した時に、それら全てのスレッドで
barrier-await
から制御が戻ります。
省略可能なaction引数は#f
(アクションなし)かサンクでなければなりません。
サンクの場合、それはバリアが開放される前の最後にbarrier-await
を呼んだ
スレッドによって、他のスレッドが走り始める前に実行されます。
name引数は任意のSchemeオブジェクトで、バリアを表示する際にのみ使われます。
{gauche.threads
}
objがバリアなら#t
を、そうでなければ#f
を返します。
{gauche.threads
}
barrierで待っているスレッドが閾値に達するか、バリアが破れるまで、
呼び出したスレッドを待たせます。
バリアが破れるのは、待っているスレッドのどれかがタイムアウトするか、 バリアのアクションから例外が投げられた時です。
timeoutとtimeout-val引数は他の同期プリミティブと同じです。
timeoutは#t
(タイムアウトなし、デフォルト)か、
絶対的時刻を指定する<time>
オブジェクトか、
相対的な秒数を指定する実数です。
timeout-valはbarrier-await
がタイムアウトかバリア破れで
戻る場合に返される値で、デフォルトは#f
です。
この手続きの戻り値は、待ちスレッド数が閾値に達して戻った場合は正確な整数、
バリアが破れた場合はtimeout-valです。
正確な整数は、barrier-await
を呼び出した時点であといくつ待ちスレッドが
必要だったかを示します。つまり、最初にバリアに到達したスレッドなら threshold - 1
、
最後に到達したなら0です。
barrier-await
を呼んだ時点で既にバリアが破れていたら、
直ちにtimeout-valが返されます。
スレッド数が閾値に達して全スレッドを解放した場合は、
バリアは初期状態に戻ります。つまり、次にbarrier-await
を呼んだスレッドは
再び待たされることになります。
バリアが破れたば場合、barrier-reset!
が呼ばれるまでバリアはずっと破れた
状態のままになります。
{gauche.threads
}
バリアbarrierが破れた状態なら#t
を、そうでなければ#f
を返します。
{gauche.threads
}
バリアを初期状態にリセットします。バリアが破れた状態なら、破れていない状態に戻ります。
バリアで待っているスレッドがいた場合、それはあたかもスレッド数が閾値に達したかのように
解放されます。
例外のいくつかのタイプは、スレッド関連の手続きから投げられます。 これらの例外は、Gaucheの例外メカニズム(例外参照)により 扱われます。
これらは元々SRFI-18で定義されました。SRFI-226もそれに倣いましたが、 名前については現代的なSchemeの名前付けを意識したものに変えています。 Gaucheではどちらの名前もサポートします。
[SRFI-226]{gauche.threads
}
スレッド関連の例外の基底クラスです。<exception>
クラスを継承しています。
スロットを1つ持っています。
<thread-exception>
: thread ¶この例外を投げたスレッド。
&thread
というコンディション名はSRFI-226での定義です。
ただし、SRFI-226のコンディションはthread
スロットは規定していません。
[SRFI-226]{gauche.threads
}
待機していたスレッドが戻る前にタイムアウトに達した時にthread-join!
によって
投げられる例外。<thread-exception>
を継承しています。
&thread-timeout
という名前はSRFI-226で定義されています。
[SRFI-226]{gauche.threads
}
ロックされるmutexが、unlocked/abandoned(ロックされておらず、放棄された状態)
であるときにmutex-lock!
により投げられる例外。
<thread-exception>
を継承しています。スロットを1つ持ちます。
<abandoned-mutex-exception>
: mutex ¶この例外の原因となったmutex。
&thread-abandoned-mutex
という名前はSRFI-226で定義されています。
[SRFI-226]{gauche.threads
}
待機していたスレッドが(thread-terminate!
により)異常終了した
場合に(thread-join!
により)投げられる例外。
<thread-exception>
を継承し、スロットを1つ持ちます。
<terminated-thread-exception>
: terminator ¶この例外の原因となったスレッドを終了したスレッド。
&thread-abandoned-mutex
という名前はSRFI-226で定義されています。
ただし、SRFI-226はterminator
スロットは定義していません。
[SRFI-226]{gauche.threads
}
待機していたスレッドが捕捉されない例外により終了された場合に
thread-join!
により投げられる例外。
<thread-exception>
を継承し、スロットを1つ持ちます。
<uncaught-exception>
: reason ¶そのスレッドの終了の原因となった例外。
&uncaught-exception
という名前はSRFI-226で定義されています。
[SRFI-226] これはスレッドセーフでないデータを複数スレッドで同時に変更しようとした場合に 投げられるコンディションです。SRFI-226で定義されていますが、 SRFIはこの状況を検出するのにオーバヘッドがある場合は検出しなくても良いとしています。 今のところ、GaucheのAPIでこのコンディションを検出するものはありません。 将来はできるかもしれません。今はこれはSRFI-226との互換性のためのみで提供されています。
[SRFI-226]{gauche.threads
}
新しい<thread-exception>
コンディションを作ります。
これはSRFI-226との互換のために用意されていますが、直接使う必要はほとんどないでしょう。
実際には<thread-exception>
のサブクラスのコンディションが使われるからです。
コンディションのthread
スロットは呼び出したスレッドで初期化されます。
[SRFI-226]{gauche.threads
}
objが<thread-exception>
(&thread
)コンディションなら#t
を、
そうでなければ#f
を返します。
[SRFI-226]{gauche.threads
}
新たな<join-timeout-exception>
(&thread-timeout
)コンディション
を作ります。
[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で定義された名前です。どちらも同じです。
[SRFI-226]{gauche.threads
}
新たな<abandoned-mutex-exception>
(&thread-abandoned-mutex
)コンディションを作ります。
[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で定義された名前です。どちらも同じです。
[SRFI-226]{gauche.threads
}
新たな<termianted-thread-exception>
(&thread-already-terminated
)コンディションを作ります。
[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で定義された名前です。どちらも同じです。
[SRFI-226]{gauche.threads
}
新たな<uncaught-exception>
(&uncaught-exception
)コンディションを作ります。
引数はコンディションのreason
スロットに保持されます。
[SRFI-18], [SRFI-21], [SRFI-226]{gauche.threads
}
objが<uncaught-exception>
のインスタンスなら#t
を、
そうでなければ#f
を返します。
uncaught-exception?
はSRFI-18/21で、
uncaught-exception-condition?
はSRFI-226で定義された名前です。どちらも同じです。
[SRFI-18], [SRFI-21], [SRFI-226]{gauche.threads
}
<uncaught-exception>
オブジェクトのreason
スロットの値を
返します。
uncaught-exception-reason
はSRFI-18/21で、
uncaught-exception-condition-reason
はSRFI-226で定義された名前です。
どちらも同じ動作です。
[SRFI-226]{gauche.threads
}
新たな<concurrent-modification-violation>
(&concurrent-modification
)コンディションを作ります。
[SRFI-226]{gauche.threads
}
objが<concurrent-modification-violation
(&concurrent-modification
)コンディションなら#t
を、
そうでなければ#f
を返します。