マルチスレッドのプログラミング

スレッドの生成と使用

スレッドパッケージは、スレッドのデータ構造、スタック、および LWP をキャッシュするので、非結合スレッドを繰り返し生成してもシステムに対する負荷は大きくなりません。

プロセスや結合スレッドの生成と比べて、非結合スレッドの生成にはかなりのオーバヘッドがあります。実際そのオーバヘッドは、1 つのスレッドを停止して他のスレッドを開始するといったコンテキストスイッチを行う場合の非結合スレッドでの同期を行うのにかかる負荷と同程度です。

したがって、必要に応じてスレッドを生成したり削除したりするほうが、専用の処理要求を待つスレッドを維持管理するより効率的です。

たとえば、RPC サーバがよい例です。RPC サーバは要求が送られてきたらスレッドを生成し、応答を返したらスレッドを削除します。要求を処理するためのスレッドを、常に維持管理しません。

スレッドの生成のオーバヘッドがプロセス生成のオーバヘッドと比べて小さいといっても、数個の命令を実行するのにかかる負荷に比べると効率的ではありません。少なくとも数千の機械語命令が続くような処理を対象にして、スレッドを生成してください。

軽量プロセス (LPW)

図 10-1 に LWP、ユーザレベル、およびカーネルレベルの関係を示します。

図 10-1 マルチスレッドのレベルと関係

Graphic

ユーザレベルのスレッドライブラリは、適切なプログラミングが行われていて、オペレーティング環境が正常に動作している限り、現在実行可能なユーザレベルのスレッド数に対して適切な数の利用可能な LWP が存在することを保証しています。しかし、ユーザレベルのスレッドと LWP の間には一対一の関係は存在しないので、ユーザレベルのスレッドはある LWP から別の LWP へと自由に移動できます。

Solaris スレッドでは、プログラマは同時にいくつのスレッドを実行させるかをスレッドライブラリに指定できます。

たとえば、最大 3 個のスレッドを同時に実行させるように指定すると、少なくとも 3 個の LWP が必要になります。3 個のプロセッサが利用可能なら、それらのスレッドは並列に実行されます。しかし、プロセッサが 1 つしか存在しないときは、オペレーティング環境が単一のプロセッサ上で 3 つの LWP を並行化します。すべての LWP がブロックされた場合は、もう 1 つ別の LWP がスレッドライブラリによって実行リソースに追加されます。

同期をとるためにユーザスレッドがブロックすると、そのスレッドが接続している LWP は、実行可能な別のスレッドと接続します。この移行にはコルーチンリンケージが使われ、システムコールは使われません。

オペレーティング環境は、どの LWP をどのプロセッサでいつ実行させるかを決定します。各プロセスにあるユーザスレッドや、ユーザスレッドがいくつ実行可能になっているかなどをオペレーティング環境は認識していません。

カーネルは、LWP のスケジューリングクラスと優先順位に従って、LWP を CPU リソースに割り当てます。同様に、スレッドライブラリはスレッドを LWP に割り当てます。

各 LWP はカーネルによって独立に振り分けられ、独立したシステムコールを実行し、独立したページフォルトを引き起こし、マルチプロセッサのシステム上では並列に動作します。

LWP の機能の中には、特別なスケジューリングクラスなどのように、スレッドからは直接には参照できないものがあります。

非結合スレッド

スレッドライブラリは、必要に応じて LWP を生成し、実行可能なスレッドを LWP に割り当てます。スレッドが割り当てられた LWP はスレッドの状態を引き継ぎ、スレッドの一連の命令を実行します。スレッドが同期機構によりブロック状態になるか、別のスレッドを実行しなければならないような状態が生じると、現在のスレッドの状態はプロセスのメモリーに退避され、スレッドライブラリはその LWP に別のスレッドを割り当てて実行します。

結合スレッド

非結合スレッドで起こりがちなことですが、スレッド数を LWP 数よりも大きくすると不利な場合があります。

たとえば、行列を並列に計算するために、行列の行を複数のスレッドに振り分ける場合を考えます。各プロセッサに 1 つの LWP が存在するが各 LWP に複数のスレッドを割り当てる場合は、各プロセッサの時間がスレッドの切り替えのために費やされます。このような場合は、各 LWP には単一のスレッドを割り当てて、行列の行を振り分けるスレッドの数を減らし、スレッドを切り替える回数を減らすほうが効果的です。

LWP に固定的に結合されたスレッドと非結合スレッドを混在させると都合がよい場合もあります。

たとえば、リアルタイムアプリケーションでは、一部のスレッドにシステム全体での優先順位を与えてリアルタイムでスケジューリングし、他のスレッドにバックグラウンドで計算を実行させます。もう 1 つの例はウィンドウシステムです。ウィンドウシステムでは大半の処理を非結合スレッドで実行し、マウスに関する処理を優先順位の高いリアルタイムの結合スレッドで実行します。

ユーザレベルのスレッドがシステムコールを発行すると、そのスレッドを実行している LWP はカーネル内部に入り、少なくともシステムコールが完了するまでの間はスレッドに接続されたままの状態となります。

結合スレッドは、非結合スレッドより負荷がかかります。結合スレッドは、結合している LWP の属性を変更することがあるので、終了時に LWP がキャッシュされることはありません。オペレーティング環境は、結合スレッドに対して生成時に新しい LWP を与え、終了時にその LWP を削除します。

結合スレッドを使用するのは、次の場合に限ってください。すなわち、結合している LWP を通してのみ利用可能なリソース (たとえば、仮想時間インタバルタイマ、代替スタック) をスレッドが必要としている場合か、スレッドをカーネルから参照可能にしてシステム内のすべての実行可能なスレッドとの関係でスケジューリングされるようにする (たとえば、リアルタイムスケジューリング) 場合です。

すべてのスレッドが同時に実行可能になることが期待される場合は、非結合スレッドを使用してください。そうすれば LWP とスレッドのリソースが効率的にキャッシュされるので、スレッドの生成と削除が高速で行われるようになります。thr_setconcurrency(3T) を使用すると、Solaris スレッドに対して同時に有効にしたいスレッド数 (目標値) を伝えることができます。

スレッドの並行度 (Solaris スレッドの場合のみ)

特に指定しなければ Solaris のスレッドは、非結合スレッドを実行するためのシステム実行リソース (LWP) の数を実行可能なスレッドの数と同じになるように調整します。この調整は完全なものではありませんが、少なくともプロセスの処理が進行することは保証されます。

同時に実行可能にすべき (コードやシステムコールを実行する) 非結合スレッドの数がわかっている場合は、thr_setconcurrency(3T) によって、その値をスレッドライブラリに指示してください。

各スレッドの生成時に THR_NEW_LWP フラグを指定して、並行度を 1 つ増やす方法もあります。

スレッドの並行度を計算するときは、プロセス間 (USYNC_PROCESS) 同期変数でブロックされている非同期スレッドも、実行可能なスレッドとして数えてください。結合スレッドは LWP と等価で、Solaris スレッドの並行度のサポートを必要としないので、実行可能なスレッドには数えません。

効率

すでにあるスレッドを再起動するよりも、thr_create(3T) で新しく生成した方が短時間ですみます。つまり、使用しないスレッドをそのまま残しておいて後で再起動するより、必要に応じて新しいスレッドを生成し使い終わったら thr_exit(3T) で終了させる方が効率的です。

スレッドの生成に関する指針

次に、スレッドを使用するときの簡単な指針を示します。