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

第 1 章 マルチスレッドの基礎

マルチスレッドという用語は、「複数の制御スレッド」または「複数の制御フロー」という意味で使われます。従来の 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)

カーネルコードやシステムコールを実行する、カーネル内部のスレッド 

結合スレッド

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 の併用

スレッドと遠隔手続き呼び出し (RPC) パッケージを組み合わせると、メモリーを共有していないマルチプロセッサ (たとえば、ワークステーションの集合体) を活用できます。この方法では、アプリケーションの分散処理を比較的簡単に実現でき、ワークステーションの集合体を 1 台のマルチプロセッサのシステムとして扱います。

たとえば、最初にあるスレッドがいくつかの子スレッドを生成します。それらの子スレッドは、それぞれが遠隔手続き呼び出しを発行して、別のワークステーション上の手続きを呼び出します。結果的に、最初のスレッドが生成した複数のスレッドは、他のコンピュータとともに並列的に実行されます。

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

並行性と並列性

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

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

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

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

マルチスレッドの構造

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

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

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

マルチスレッドをサポートするインタフェースは、サブルーチンライブラリで提供されます (POSIX スレッド用は libpthread で、Solaris スレッド用は 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 は生成されません。

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

Graphic

各 LWP はカーネルプールの中のカーネルリソースであり、スレッドに割り当てられ (接続され) たり、割り当てを解除され (切り離され) たりします。この割り当て / 割り当て解除はスレッド単位ごとに、スレッドがスケジュールされるか、生成または破棄されたときに行われます。

スケジューリング

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

SCHED_FIFOSCHED_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 つの同期モデルがあります。

64 ビットアーキテクチャ

アプリケーション開発者にとって、Solaris 64 ビット版と 32 ビット版のオペレーティング環境の主な相違点は、使用する C 言語のデータ型です。64 ビットのデータ型では、long 型とポインタが 64 ビット幅の、LP64 モデルを使用します。その他の基本データ型は 32 ビット版と同じです。32 ビットのデータ型は、intlong、およびポインタが 32 ビットの、ILP 32 モデルを使用します。

64 ビット環境を使用する場合の、主な特徴と注意すべき点について、以下に簡単に説明します。