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

マルチスレッドの基本概念

並行性と並列性

マルチスレッドプロセスがシングルプロセッサ上で動作する場合は、プロセッサが実行リソースを各スレッドに順次切り替えて割り当てるため、プロセスの実行状態は並行的になります。

同じマルチスレッドプロセスが共有メモリー方式のマルチプロセッサ上で動作する場合は、プロセス中の各スレッドが別のプロセッサ上で同時に走行するため、プロセスの実行状態は並列的になります。

プロセスのスレッド数がプロセッサ数と等しいか、それ以下であれば、スレッドをサポートするシステム (スレッドライブラリ) とオペレーティング環境は、各スレッドがそれぞれ別のプロセッサ上で実行されることを保証します。

たとえば、スレッドとプロセッサが同数で行列の乗算を行う場合は、各スレッド (と各プロセッサ) が 1 つの行の計算を担当します。

マルチスレッドの構造

従来の UNIX でもスレッドという概念はすでにサポートされています。各プロセスは 1 つのスレッドを含むので、複数のプロセスを使うようにプログラミングすれば、複数のスレッドを使うことになります。しかし、1 つのプロセスは 1 つのアドレス空間でもあるので、1 つのプロセスを生成すると 1 つの新しいアドレス空間が作成されます。

新しいプロセスを生成するかわりに、スレッドを生成するとシステムへの負荷は小さくなります。これは、新たに生成されるスレッドが現在のプロセスのアドレス空間を使用するからです。スレッドの切り替えに要する時間は、プロセスの切り替えに要する時間よりも短くて済みます。その理由の 1 つは、スレッドを切り替える上でアドレス空間を切り替える必要がないことです。

同じプロセスに属するスレッド間の通信は簡単に実現できます。それらのスレッドは、アドレス空間を含めあらゆるものを共有しているからです。したがって、あるスレッドで生成されたデータを、他のすべてのスレッドがただちに利用できます。

マルチスレッドをサポートするインタフェースは、サブルーチンライブラリで提供されます (POSIX スレッド用は libpthread で、Solaris スレッド用は libthread です)。カーネルレベルとユーザーレベルのリソースを切り離すことによって、マルチスレッドは柔軟性をもたらします。

ユーザーレベルのスレッド

スレッドは、マルチスレッドのプログラミングにおいて基本となるプログラミングインタフェースです。 [ユーザレベルのスレッドという呼称は、システムプログラマだけが関係するカーネルレベルのスレッドと区別するためのものです。このマニュアルは、アプリケーションプログラマ向けであるため、カーネルレベルのスレッドについては触れません。] スレッド は、そのプロセス内でのみ参照可能であり、アドレス空間や開いているファイルといったプロセスリソースは、すべて共有されます。スレッドごとに固有な状態としては次のものがあります。

スレッドはプロセスの命令とそのデータの大半を共有するので、あるスレッドが行なった共有データの変更は、プロセスの他のすべてのスレッドから参照できます。スレッドが自分と同じプロセス内の他のスレッドとやり取りを行う場合は、オペレーティング環境を介する必要はありません。

デフォルトでは、スレッドは軽量です。しかし、スレッドをより厳格に制御したいアプリケーションでは (たとえば、スケジューリングの方針をより厳密に適用したい場合など)、スレッドを結合できます。アプリケーションがスレッドを実行リソースに結合すると、そのスレッドはカーネルのリソースとなります (詳細は、システムスコープ (結合スレッド)を参照してください)。

以下に、ユーザーレベルのスレッドの利点を要約します。

軽量プロセス (LPW)

スレッドライブラリは、カーネルによってサポートされる軽量プロセス (LWP) と呼ばれる制御スレッドを基礎としています。 LWP は、コードまたはシステムコールを実行する仮想 CPU と見なすことができます。

通常、スレッドを使用するプログラミングで LWP を意識する必要はありません。以下に述べる LWP の説明は、プロセススコープ (非結合スレッド)で述べるスケジューリングスコープの違いを理解する際の参考にしてください。

fopen() や fread() などの stdio ライブラリルーチンが open() や read() などのシステムコールを使用するのと同じように、スレッドインタフェースも LWP インタフェースを使用します。

軽量プロセス (LWP) はユーザーレベルとカーネルレベルの橋渡しをします。各プロセスは、1 つ以上の LWP で構成されます。各 LWP は、1 つ以上のユーザースレッドを実行します (図 1–1 を参照)。

図 1–1 ユーザーレベルスレッドと軽量プロセス

軽量プロセス (LWP)、ユーザレベル、およびカーネルレベルの関係を示しています。

各 LWP はカーネルプールの中のカーネルリソースであり、スレッドに割り当てられ たり、割り当てを解除されたりします。

スケジューリング

POSIX はスケジューリングの方針として、先入れ先出し (SCHED_FIFO)、ラウンドロビン (SCHED_RR)、カスタム (SCHED_OTHER) の 3 つを規定しています。SCHED_FIFO は待ち行列ベースのスケジューラで、優先レベルごとに異なる待ち行列をもっています。SCHED_RR は FIFO に似ていますが、各スレッドに実行時間の制限があるという点が異なります。

SCHED_FIFO と SCHED_RR は両方とも POSIX のリアルタイム拡張機能です。SCHED_OTHER がデフォルトのスケジューリング方針です。

SCHED_OTHER 方針の詳細については、LWP とスケジューリングクラスを参照してください。

スケジューリングスコープ (スケジューリングの適応範囲) として、プロセススコープ (非結合スレッド用) とシステムスコープ (結合スレッド用) の 2 つが使用できます。スコープの状態が異なるスレッドが同じシステムに同時に存在でき、さらに同じプロセスにも同時に存在できます。通常、スコープは適応範囲を設定します。その範囲内でスレッドの方針が有効となります。

プロセススコープ (非結合スレッド)

PTHREAD_SCOPE_PROCESS スレッドは、非結合スレッドとして生成されます。これらのスレッドと LWP の結合は、スレッドライブラリによって管理されます。

通常は PTHREAD_SCOPE_PROCESS スレッドを使用します。これらのスレッドは、特定の LWP で実行する制約を持たず、THR_BOUND フラグを指定しないで生成された Solaris スレッドと等価です。 各スレッドと LWP の結合は、スレッドライブラリによって決定されます。

システムスコープ (結合スレッド)

PTHREAD_SCOPE_SYSTEM スレッドは、結合スレッドとして生成されます。結合スレッドは、LWP に永久に結合されます。

それぞれの結合スレッドは、初めから終わりまで特定の LWP に結び付けられています。これは Solaris スレッドを THR_BOUND 状態で生成するのと同じことです。スレッドを結合すれば、リアルタイムスケジューリング専用のスケジューリング属性を使用できます。


注 –

結合と非結合のいずれのスレッドの場合でも、他のプロセスからスレッドに直接アクセスしたり、他のプロセスに移動したりできません。


取り消し

スレッド取り消しによって、スレッドはそのプロセス中の他のスレッドの実行を終了させることができます。取り消しの対象となるスレッドは、取り消し要求を保留しておき、取り消しに応じる際にアプリケーション固有のクリーンアップを実行できます。

pthread 取り消し機能では、スレッドの非同期終了または遅延終了が可能です。非同期取り消しはいつでも起こりうるものですが、遅延取り消しは定義されたポイントでのみ発生します。遅延取り消しがデフォルトタイプです。

同期

同期を使用すると、並行的に実行されているスレッドに関して、プログラムの流れと共有データへのアクセスを制御することが可能になります。

相互排他ロック (mutex ロック)、読み取り / 書き込みロック、条件変数、セマフォという 4 つの同期モデルがあります。