マルチスレッドという用語は、「複数の制御スレッド」または「複数の制御フロー」という意味で使われます。従来の UNIX のプロセスは、1 つの制御スレッドで動作していましたが、マルチスレッド (MT) では、1 つのプロセスを複数のスレッドに分割し、それぞれのスレッドが独立に動作します。
プログラムをマルチスレッド化すると、次のような利点が生まれます。
アプリケーションの応答性が改善される
マルチプロセッサをより効率的に利用できる
プログラム構造が改善される
システムリソースが節約できる
この章では、マルチスレッドについての用語、利点、および概念を説明します。こうした事柄について理解できている方は、第 2 章「スレッドを使った基本プログラミング」に進んでください。
表 1-1 で、このマニュアルで使われている主な用語を紹介します。
表 1-1 マルチスレッドに関する用語の定義
用語 |
定義 |
---|---|
プロセス |
fork(2) システムコールで生成される UNIX 環境 (ファイル記述子やユーザ ID などのコンテキスト) で、プログラムを実行するために設定される。 |
プロセスのコンテキスト内で実行されるひとまとまりの命令 |
|
pthread (POSIX スレッド) |
POSIX 1003.1c に準拠したスレッドインタフェース |
Solaris スレッド |
POSIX に準拠しない、Sun MicrosystemsTM のスレッドインタフェース。pthread より先に存在 |
1 プロセス 1 スレッドで動作させること |
|
1 プロセス複数スレッドで動作させること |
|
(カーネル空間に対応する) ユーザ空間に位置し、スレッドライブラリルーチンによって管理されるスレッド |
|
カーネルコードやシステムコールを実行する、カーネル内部のスレッド |
|
LWP に固定的に結合したスレッド |
|
カーネルのサポートなしでコンテキストが非常にすばやく切り替わるデフォルトの Solaris スレッド |
|
属性オブジェクト |
不透明なデータ型と関連操作のための関数が含まれ、POSIX スレッド、mutex、条件変数の調整可能な部分を共通化するために使用される |
相互排他ロック |
共有データへのアクセスをロック / ロック解除する機能 |
条件変数 |
状態が変化するまでスレッドをブロックする機能 |
メモリーを使用する同期機構 |
|
並列性 |
2 つ以上のスレッドが同時に実行されている状態を表す概念 |
並行性 |
2 つ以上のスレッドが進行過程にある状態を表す概念。仮想的な並列性としてタイムスライスを包含する、一般化された形の並列性 |
マルチスレッドのプログラミングという概念の起源は、少なくとも 1960 年代にまでさかのぼります。マルチスレッドが UNIX システム上で開発されたのは 1980 年代の中期になります。マルチスレッドの意味とそのサポートに必要な機能については合意がありますが、マルチスレッドを実装するためのインタフェースはさまざまです。
この数年間、POSIX (Portable Operating System Interface) 1003.4a というグループによって、スレッドプログラミングの標準化についての作業が行われてきました。この標準はいまや承認されるに至っています。この『マルチスレッドのプログラミング』は、POSIX 規格の P1003.1b 最終草稿 14 (リアルタイム) と P1003.1c 最終草稿 10 (マルチスレッド) に基づいています。
このマニュアルは、POSIX スレッド (pthread とも言います) と Solaris スレッドの両方を対象にしています。Solaris スレッドは Solaris 2.4 以降のリリースで利用できますし、POSIX スレッドとも機能的に異なりません。しかし、Solaris スレッドより POSIX スレッドのほうが移植性が高いので、このマニュアルではマルチスレッドを POSIX の立場から解説しています。Solaris スレッドに固有な事柄については、第 8 章「Solaris スレッドを使ったプログラミング」で説明します。
互いに独立した処理を含んでいるプログラムは、設計を変更して、個々の処理をスレッドとして定義できます。たとえば、マルチスレッド化された GUI のユーザは、ある処理が完了しないうちに別の処理を開始できます。
スレッドによって並行化されたアプリケーションでは、ほとんどの場合、利用可能なプロセッサ数を考慮する必要はありません。そのようなアプリケーションでは、プロセッサを追加するだけで性能が目に見えて改善されます。
行列の乗算のような並列性の度合いが高い数値計算アプリケーションは、マルチプロセッサ上でスレッドを実装することにより、処理速度を大幅に改善できます。
ほとんどのプログラムは、単一のスレッドで実現するよりも複数の独立した (あるいは半独立の) 実行単位の集合体として実現した方が効果的に構造化されます。マルチスレッド化されたプログラムの方が、シングルスレッド化されたプログラムよりもユーザのさまざまな要求に柔軟に対応できます。
共有メモリーを通して複数のプロセスが共通のデータを利用するようなプログラムは、複数の制御スレッドを使用していることになります。
しかし、各プロセスは完全なアドレス空間とオペレーティング環境上での状態をもちます。そのような大規模な状態情報を作成して維持しなければならないという点で、プロセスはスレッドに比べて時間的にも空間的にも不利です。
さらに、プロセス本来の独立性のため、他のプロセスに属するスレッドと通信したり同期を取ったりする際に、プログラマは面倒な処理をしなくてはなりません。
スレッドと遠隔手続き呼び出し (RPC) パッケージを組み合わせると、メモリーを共有していないマルチプロセッサ (たとえば、ワークステーションの集合体) を活用できます。この方法では、アプリケーションの分散処理を比較的簡単に実現でき、ワークステーションの集合体を 1 台のマルチプロセッサのシステムとして扱います。
たとえば、最初にあるスレッドがいくつかの子スレッドを生成します。それらの子スレッドは、それぞれが遠隔手続き呼び出しを発行して、別のワークステーション上の手続きを呼び出します。結果的に、最初のスレッドが生成した複数のスレッドは、他のコンピュータとともに並列的に実行されます。
マルチスレッドプロセスがシングルプロセッサ上で動作する場合は、プロセッサが実行リソースを各スレッドに順次切り替えて割り当てるため、プロセスの実行状態は並行的になります。
同じマルチスレッドプロセスが共有メモリー方式のマルチプロセッサ上で動作する場合は、プロセス中の各スレッドが別のプロセッサ上で同時に走行するため、プロセスの実行状態は並列的になります。
プロセスのスレッド数がプロセッサ数と等しいか、それ以下であれば、スレッドをサポートするシステム (スレッドライブラリ) とオペレーティング環境は、各スレッドがそれぞれ別のプロセッサ上で実行されることを保証します。
たとえば、スレッドとプロセッサが同数で行列の乗算を行う場合は、各スレッド (と各プロセッサ) が 1 つの行の計算を担当します。
従来の UNIX でもスレッドという概念はすでにサポートされています。各プロセスは 1 つのスレッドを含むので、複数のプロセスを使うようにプログラミングすれば、複数のスレッドを使うことになります。しかし、1 つのプロセスは 1 つのアドレス空間でもあるので、1 つのプロセスを生成すると 1 つの新しいアドレス空間が作成されます。
新しいプロセスを生成するのに比べると、スレッドを生成するのはシステムへの負荷ははるかに小さくなります。これは、新たに生成されるスレッドが現在のプロセスのアドレス空間を使用するからです。スレッドの切り替えに要する時間は、プロセスの切り替えに要する時間よりもかなり短いです。その理由の 1 つは、スレッドを切り替える上でアドレス空間を切り替える必要がないことです。
同じプロセスに属するスレッド間の通信は簡単に実現できます。それらのスレッドは、アドレス空間を含めあらゆるものを共有しているからです。したがって、あるスレッドで生成されたデータを、他のすべてのスレッドがただちに利用できます。
マルチスレッドをサポートするインタフェースは、サブルーチンライブラリで提供されます (POSIX スレッド用は libpthread で、Solaris スレッド用は libthread です)。カーネルレベルとユーザレベルのリソースを切り離すことによって、マルチスレッドは柔軟性をもたらします。
スレッドは、マルチスレッドのプログラミングにおいて基本となるプログラミングインタフェースです。ユーザレベルのスレッド [ユーザレベルのスレッドという呼称は、システムプログラマだけが関係するカーネルレベルのスレッドと区別するためのものです。このマニュアルは、アプリケーションプログラマ向けであるため、カーネルレベルのスレッドについては触れません。] はユーザ空間で処理されるため、カーネルのコンテキストスイッチの負荷を増やすことはありません。何百ものスレッドを使用するようなアプリケーションでも、カーネルのリソースをそれほど使用しなくてもすみます。どのくらいの量のカーネルリソースをアプリケーションが必要とするかは、主にアプリケーション自体の性質で決まります。
スレッドは、それらが存在するプロセスの内部からだけ参照でき、アドレス空間や開いているファイルなどすべてのプロセスリソースを共有します。スレッドごとに固有な状態としては次のものがあります。
スレッドはプロセスの命令とそのデータの大半を共有するので、あるスレッドが行なった共有データの変更は、プロセスの他のすべてのスレッドから参照できます。スレッドが自分と同じプロセス内の他のスレッドとやり取りを行う場合は、オペレーティング環境を介する必要はありません。
デフォルトでは、スレッドは非常に軽量です。しかし、スレッドをより厳格に制御したいアプリケーションでは (たとえば、スケジューリングの方針をより厳密に適用したい場合など)、スレッドを結合できます。アプリケーションがスレッドを実行リソースに結合すると、そのスレッドはカーネルのリソースとなります (詳細は、「システムスコープ (結合スレッド)」を参照してください)。
以下に、ユーザレベルのスレッドの利点を要約します。
独自のアドレス空間を生成する必要がないので、生成に伴うシステムへの負荷が小さくてすみます。わずかな量の仮想メモリを、実行時のアドレス空間に確保するだけです。
同期がカーネルレベルでなくユーザレベルでとられるため、高速な同期が可能です。
スレッドライブラリ libpthread と libthread によって、簡単に管理できます。
スレッドライブラリは、カーネルによってサポートされる軽量プロセス (LWP) と呼ばれる制御スレッドを基礎としています。LWP は、コードやシステムコールを実行する仮想的な CPU と考えることができます。
通常、スレッドを使用するプログラミングで LWP を意識する必要はありません。以下に述べる LWP の説明は、「プロセススコープ (非結合スレッド)」で述べるスケジューリングスコープの違いを理解する際の参考にしてください。
Solaris 2、Solaris 7、および Solaris 8 オペレーティング環境の LWP と SunOSTM 4.0 LWP ライブラリの LWP は同じものではありません。後者は Solaris 2、Solaris 7、および Solaris 8 オペレーティング環境ではサポートされていません。
fopen() や fread() などの stdio ライブラリルーチンが open() や read() などのシステムコールを使用するのと同じように、スレッドインタフェースも LWP インタフェースを使用します。
軽量プロセス (LWP) はユーザレベルとカーネルレベルの橋渡しをします。各プロセスは 1 つ以上の LWP を含み、それぞれの LWP は 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 方針、および POSIX の SCHED_FIFO 方針と SCHED_RR 方針のプロパティのエミュレートについては、「LWP とスケジューリングクラス」を参照してください。
スケジューリングスコープ (スケジューリングの適応範囲) として、プロセススコープ (非結合スレッド用) とシステムスコープ (結合スレッド用) の 2 つが使用できます。スコープの状態が異なるスレッドが同じシステムに同時に存在でき、さらに同じプロセスにも同時に存在できます。通常、スコープは適応範囲を設定します。その範囲内でスレッドの方針が有効となります。
非結合スレッドは PTHREAD_SCOPE_PROCESS として生成されます。このようなスレッドはユーザ空間内でスケジュールされ、LWP プールの中の使用可能な LWP に対して接続されたり切り離されたりします。LWP はこのプロセス内のスレッドにのみ使用可能です。つまり、スレッドはこれらの LWP にスケジュールされるわけです。
通常は PTHREAD_SCOPE_PROCESS スレッドを使用します。そうすれば、LWP の間でスレッドの使い回しができるので、スレッドの効率が良くなります (また、Solaris スレッドを THR_UNBOUND 状態で生成するのと同じことになります)。スレッドライブラリは、他のスレッドを考慮しつつ、どのスレッドがカーネルのサービスを受けるかを決定します。
結合スレッドは PTHREAD_SCOPE_SYSTEM として生成されます。結合スレッドは、LWP に永久に結合されます。
それぞれの結合スレッドは、初めから終わりまで特定の LWP に結び付けられています。これは Solaris スレッドを THR_BOUND 状態で生成するのと同じことです。スレッドを結合することによって、スレッドに代替シグナルスタックを与えたり、リアルタイムスケジューリングで特別なスケジューリング属性を使用したりできます。すべてのスケジューリングは、オペレーティング環境で行われます。
結合と非結合のいずれのスレッドの場合でも、他のプロセスからスレッドに直接アクセスしたり、他のプロセスに移動したりできません。
スレッド取り消しによって、スレッドはそのプロセス中の他のスレッドの実行を終了させることができます。取り消しの対象となるスレッドは、取り消し要求を保留しておき、取り消しに応じる際にアプリケーション固有のクリーンアップを実行できます。
pthread 取り消し機能では、スレッドの非同期終了または遅延終了が可能です。非同期取り消しはいつでも起こりうるものですが、遅延取り消しは定義されたポイントでのみ発生します。遅延取り消しがデフォルトタイプです。
同期を使用すると、並行的に実行されているスレッドに関して、プログラムの流れと共有データへのアクセスを制御することが可能になります。
相互排他ロック (mutex ロック)、読み取り/書き込みロック、条件変数、セマフォという 4 つの同期モデルがあります。
読み取り / 書き込みロックを使用することによって、プロテクトのかけられている共有リソースに対する並行読み取りや排他書き込みが可能になります。リソースを変更するには、スレッドがまず排他書き込みロックを獲得する必要があります。すべての読み取りロックが開放されない限り、排他書き込みロックは許可されません。
アプリケーション開発者にとって、Solaris 64 ビット版と 32 ビット版のオペレーティング環境の主な相違点は、使用する C 言語のデータ型です。64 ビットのデータ型では、long 型とポインタが 64 ビット幅の、LP64 モデルを使用します。その他の基本データ型は 32 ビット版と同じです。32 ビットのデータ型は、int、long、およびポインタが 32 ビットの、ILP 32 モデルを使用します。
64 ビット環境を使用する場合の、主な特徴と注意すべき点について、以下に簡単に説明します。
巨大な仮想アドレス空間
64 ビット環境では、プロセスは最大 64 ビットすなわち 18E (エクサ) バイトの仮想アドレス空間を持つことができます。これは、現在の 32 ビット環境における最大 4G バイトの 40 億倍です。ただし、ハードウェアの制約上、一部のプラットフォームでは 64 ビットのアドレス空間を完全にはサポートしていません。
巨大な仮想アドレス空間では、デフォルトのスタックサイズ (32 ビット版では 1M バイト、64 ビット版では 2M バイト) で作成できるスレッドの数も多くなります。デフォルトのスタックサイズで作成できるスレッドの数は、32 ビットシステムで約 2000、64 ビットシステムで約 8 兆です。
カーネルメモリーの読み取り
カーネルは LP64 オブジェクトであり、内部では 64 ビットのデータ構造を使用するため、libkvm、/dev/mem、または/dev/kmem を使用する既存の 32 ビットアプリケーションは正常に動作しません。これらのアプリケーションは 64 ビットプログラムに変換する必要があります。
/proc の制限
/proc を使う 32 ビットプログラムでは、32 ビットのプロセスは見ることができますが、64 ビットのプロセスを解釈することはできません。プロセスを記述する既存のインタフェースとデータ構造は、64 ビットのプロセスを収容できるだけの容量がありません。これらのプログラムが 32 ビットと 64 ビットの両プロセスに対して動作できるようにするには、64 ビットプログラムとしてコンパイルし直す必要があります。
64 ビットのライブラリ
32 ビットのアプリケーションは 32 ビットのライブラリと、64 ビットのアプリケーションは 64 ビットのライブラリと、リンクしている必要があります。システムライブラリには、古くなったもの以外はすべて、32 ビットと 64 ビットの両方が用意されています。ただし、64 ビットのライブラリは静的な形式では提供されていません。
64 ビット演算
32 ビット版の従来の Solaris でも、64 ビット演算が行えましたが、64 ビット版では、整数の演算やパラメタの引き渡しに、マシンの 64 ビットレジスタを全面的に使用できるようになりました。
大容量のファイル
アプリケーションが必要としているのが大容量ファイルのサポートだけである場合は、32 ビットのままで大容量ファイルのインタフェースを使用することもできます。ただし、64 ビットの機能を最大限に活かすためには 64 ビットに変換することをお勧めします。