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

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

マルチスレッドという用語は、「複数の制御スレッド」または「複数の制御フロー」という意味で使われます。従来の UNIX プロセスは単一の制御スレッドを含んでいたのに対して、マルチスレッド (MT) は 1 つのプロセスを複数の実行スレッドに分割します。これらのスレッドはそれぞれ独立して実行されます。

この章では、マルチスレッドについての用語、利点、および概念を説明します。すぐにマルチスレッドを使用する場合は、第 2 章スレッドを使った基本プログラミングへ進んでください。

マルチスレッドプログラミングに関する詳細な情報が必要な場合は、「はじめに」の 「関連情報」の節を参照してください。

マルチスレッドに関する用語の定義

表 1–1 では、このマニュアルで使用する用語の一部について解説します。

表 1–1 マルチスレッドに関する用語の定義

用語 

定義 

プロセス 

fork(2) システムコールで生成される UNIX 環境 (ファイル記述子やユーザー ID などのコンテキスト) で、プログラムを実行するために設定されます。

スレッド 

プロセスのコンテキスト内で実行されるひとまとまりの命令。 

POSIX pthreads

POSIX スレッドに準拠したスレッドインタフェース。詳細については、「Solaris のマルチスレッドライブラリと標準」を参照してください。

Solaris スレッド

POSIX スレッドに準拠しない、Sun Microsystems のスレッドインタフェース。pthread の祖先。 

シングルスレッド

1 プロセス 1 スレッドで動作させること。実行は逐次処理で行われ、制御スレッドが 1 つに制限されます。 

マルチスレッド

1 プロセス複数スレッドで動作させること。実行は複数の制御スレッドで行われ、並列または並行処理が使用されます。 

ユーザーレベルまたはアプリケーションレベルのスレッド

(カーネル空間に対応する) ユーザー空間のスレッドルーチンによって管理されるスレッド。ユーザースレッドの作成および処理には、POSIX pthread や Solaris スレッドの API が使用されます。このマニュアルでは (また一般にも)、スレッドとはユーザーレベルのスレッドを指します。 


注 –

このマニュアルはアプリケーションプログラマ向けであるため、カーネルスレッドのプログラミングについては触れません。


軽量プロセス

カーネルコードとシステムコールを実行するカーネルスレッド (LWP)。LWP はシステムスレッドのスケジューラによって管理され、アプリケーションプログラマが直接制御することはできません。Solaris 9 以降、ユーザーレベルのすべてのスレッドに専用の LWP が用意されるようになりました。これは 1:1 スレッドモデルとも呼ばれます。 

結合スレッド (廃止された用語)

Solaris 9 より前の、単一の LWP に永続的に結合されたユーザーレベルのスレッド。Solaris 9 以降は、すべてのスレッドに専用の LWP が用意されるようになったため、すべてのスレッドが結合スレッドです。非結合スレッドの概念は存在しなくなりました。 

非結合スレッド (廃止された用語)

Solaris 9 より前の、単一の LWP に結合されているとは限らないユーザーレベルのスレッド。Solaris 9 以降は、すべてのスレッドに専用の LWP が用意されるようになったため、非結合スレッドの概念は存在しなくなりました。 

属性オブジェクト 

不透明なデータ型と関連操作のための関数が含まれます。これらのデータ型および関数は、POSIX スレッド、相互排他ロック (mutex)、条件変数の調整可能な部分を共通化するために使用されます。 

相互排他ロック

共有データへのアクセスをロック / ロック解除するために使用されるオブジェクト。このようなオブジェクトは mutex とも呼ばれます。 

条件変数 

状態が変化するまでスレッドをブロックするために使用されるオブジェクト。 

読み取り/書き込みロック 

共有データに対して複数の読み取り専用アクセスを許可するが、共有データを変更する場合は排他的アクセスを許可するために使用されるオブジェクト。 

計数型セマフォー

複数のスレッドからの共有リソースへのアクセスを調整するために 0 以上の整数カウントが使用される、メモリーベースの同期機構。 

並列性 

2 つ以上のスレッドが同時に実行されている状態を表す概念。

並行性 

2 つ以上のスレッドが進行過程にある状態を表す概念。仮想的な並列性としてタイムスライスを包含する、一般化された形の並列性。

Solaris のマルチスレッドライブラリと標準

マルチスレッドプログラミングの概念の起源は、少なくとも 1960 年代にさかのぼります。その後、UNIX システム上でマルチスレッドプログラミングが行われるようになったのは、1980 年代中盤のことです。マルチスレッドの意味とそのサポートに必要な機能に関する合意がありますが、マルチスレッドを実装するためのインタフェースはさまざまです。

この数年間、POSIX (Portable Operating System Interface) 1003.4a によって、スレッドプログラミングの標準化についての作業が行われてきました。この標準は最終的に承認され、現在は Single UNIX Specification (SUS) の一部になっています。最新の仕様は、Open Group の Web サイトで入手できます。Solaris 10 リリースから、Solaris OS は Open Group の UNIX 03 Product Standard (SUSv3) に準拠しています。

POSIX 標準が承認される前、Solaris のマルチスレッド API は、Solaris libthread ライブラリで実装されていました。Sun が開発したこのライブラリは、そのあとで UNIX International (UI) スレッド標準の基礎になっています。この libthread ライブラリは、1993 年に Solaris 2.2 リリースで導入されました。1995 年の Solaris 2.5 リリースでは、POSIX 標準のサポートが libpthread API によって追加され、それ以降は両方の API が使用可能になりました。Solaris 10 リリースからは、libthread ライブラリと libpthread ライブラリが標準の libc C ライブラリに統合されました。

libthread ライブラリと libpthread ライブラリは、実行環境とコンパイル環境の両方の下位互換を提供するために維持されています。libthread.so.1 および libpthread.so.1 共有オブジェクトは、libc.so.1 に対するフィルタとして実装されています。詳細は、libthread(3LIB)libpthread(3LIB) のマニュアルページを参照してください。

両方のスレッドライブラリがサポートされていますが、通常は POSIX ライブラリを使用するようにしてください。threads(5) のマニュアルページには、POSIX スレッドと Solaris スレッドの相違点と類似点が記載されています。

この『マルチスレッドのプログラミング』は、POSIX 標準 IEEE Std 1003.1: 2001 の最新のリビジョン (ISO/IEC 9945:2003 とも Single UNIX Specification Version 3 とも呼ばれる) に基づいています。

Solaris スレッド固有の機能については、第 6 章Solaris スレッドを使ったプログラミングを参照してください。

マルチスレッドの利点

この節では、マルチスレッドの利点について概説します。

プログラムのマルチスレッド化には、次のような利点があります。

アプリケーションの応答性の改善

互いに独立した処理を含んでいるプログラムは、設計を変更して、個々の独立した処理をスレッドとして定義できます。たとえば、マルチスレッド化された GUI のユーザーは、ある処理が完了しないうちに別の処理を開始できます。

マルチプロセッサの効率的な利用

スレッドによって並行化されたアプリケーションでは、ほとんどの場合、利用可能なプロセッサ数を考慮する必要はありません。アプリケーションのパフォーマンスはプロセッサが増えると透過的に向上します。これは、利用可能なプロセッサ数に応じたスレッドのスケジューリングがオペレーティングシステムによって処理されるためです。マルチコアプロセッサやマルチスレッドプロセッサが使用可能な場合は、それらのコアやスレッドが OS によってプロセッサとして見なされるため、マルチスレッドアプリケーションの性能はそれに応じて向上します。

行列の乗算のような並列性の度合いが高い数値計算アルゴリズムや数値計算アプリケーションでは、マルチプロセッサ上でスレッドを実装することにより、処理速度を大幅に改善できます。


注 –

このマニュアルでは、特に断りのない限り、マルチプロセッサの説明はマルチコアプロセッサとマルチスレッドプロセッサにも該当します。


プログラム構造の改善

ほとんどのプログラムは、単一のスレッドで実現するよりも複数の独立した (あるいは半独立の) 実行単位の集合体として実現した方が効果的に構造化されます。たとえば、多様なタスクを実行するスレッド化されていないプログラムでは、そのコードの大部分がそれらのタスクの調整のためだけに費やされる可能性があります。それらのタスクをスレッドとしてプログラミングすれば、コードを簡略化できます。マルチスレッド化されたプログラム (特に、複数の並行ユーザーにサービスを提供するプログラム) の方が、シングルスレッド化されたプログラムよりもユーザーのさまざまな要求に柔軟に対応できます。

システムリソースの節約

共有メモリーを通して複数のプロセスが共通のデータを利用するようなプログラムは、複数の制御スレッドを使用していることになります。

しかし、各プロセスは完全なアドレス空間とオペレーティング環境上での状態をもちます。そのような大規模な状態情報を作成して維持しなければならないという点で、プロセスはスレッドに比べて時間的にも空間的にも不利です。

さらに、プロセス本来の独立性のため、ほかのプロセスに属するスレッドとの通信や同期など、プログラマに課される処理が多くなります。この処理には、異なるプロセスに含まれるスレッド間の通信の処理や、それらのスレッドの操作の同期などがあります。これらのスレッドが同じプロセスに含まれていると、通信や同期がはるかに簡単になります。

スレッドと RPC の併用

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

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


注 –

分散システム全体にわたって動作するアプリケーションのマルチスレッド化を実現するには、MPI (Message Processing Interface) の方が効果的なアプローチになることがあります。MPI の詳細は、http://www-unix.mcs.anl.gov/mpi/ を参照してください。

Sun HPC ClusterToolsTM ソフトウェアには、MPI のオープンソース実装である OMPI (Open MPI Message Passing Interface) が含まれています。ClusterTools の詳細は、Sun HPC ClusterTools の製品ページ を参照してください。


マルチスレッドの概念

この節では、マルチスレッドの基本概念について説明します。

並行性と並列性

マルチスレッドプロセスがシングルプロセッサ上で動作する場合は、プロセッサが実行リソースを各スレッドに順次切り替えて割り当てるため、プロセスの実行状態は並行的になります。この並行性は、複数のスレッドが進行しているが、実際にはこれらのスレッドが同時には実行されていないことを示しています。スレッド間の切り替えが高速なため、スレッドが同時に実行されているように見える可能性があります。

同じマルチスレッドプロセスが共有メモリー方式のマルチプロセッサ上で動作する場合は、プロセス中の各スレッドが別のプロセッサ上で実行されるため、プロセスの実行状態は並列的になります。これが本来の同時実行です。プロセス内のスレッド数が利用可能なプロセッサ数に等しいか、それより少ない場合は、オペレーティングシステムのスレッドサポートシステムによって、各スレッドがそれぞれ別のプロセッサ上で実行されることが保証されます。たとえば、4 つのスレッドを使用してプログラミングされ、2 つのデュアルコアプロセッサを備えたシステム上で実行される行列の乗算では、結果の行を同時に計算するために 4 つのプロセッサコア上で各ソフトウェアスレッドを同時に実行できます。

マルチスレッドの構造

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

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

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

ただしこのようにデータを共有すると、プログラマにとって別の一連の課題が発生します。データが複数のスレッドから一度に変更されたり、あるスレッドによって変更されている間に別のスレッドから同時に読み取られたりすることのないようにスレッドを同期化する必要があります。詳細については、「スレッド同期」を参照してください。

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

スレッドは、マルチスレッドプログラミングにおける基本プログラミングインタフェースです。スレッドは、それらが存在するプロセスの内部からだけ参照でき、アドレス空間や開いているファイルなどすべてのプロセスリソースを共有します。

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

スレッドごとに固有な状態としては次のものがあります。

スレッドはプロセスの命令とプロセスデータの大部分を共有します。このため、あるスレッドによって共有データに加えられた変更は、プロセス内のほかのスレッドから認識できます。スレッドが自分と同じプロセス内の他のスレッドとやり取りを行う場合は、オペレーティング環境を介する必要はありません。


注 –

ユーザーレベルのスレッドという呼称は、システムプログラマだけが関係するカーネルレベルのスレッドと区別するためのものです。このマニュアルは、アプリケーションプログラマ向けであるため、カーネルレベルのスレッドについては触れません。


スレッドのスケジューリング

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

SCHED_FIFOSCHED_RR は両方とも POSIX のリアルタイム拡張機能です。これらの方針で実行されているスレッドは、通常は特殊な特権が必要な Solaris のリアルタイム (RT) スケジューリングクラスに含まれます。SCHED_OTHER がデフォルトのスケジューリングポリシーです。SCHED_OTHER 方針で実行されているスレッドは、従来の Solaris タイムシェアリング (TS) スケジューリングクラスに含まれます。

Solaris は、ほかのスケジューリングクラス、つまり対話型タイムシェアリング (IA) クラス、公平分配 (FSS) クラス、および固定優先順位 (FX) クラスを提供しています。ここでは、これらの特殊なクラスについては説明していません。詳細は、Solaris priocntl(2) のマニュアルページを参照してください。

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

次の 2 つのスケジューリングスコープが使用できます。 プロセススコープ (PTHREAD_SCOPE_PROCESS) とシステムスコープ (PTHREAD_SCOPE_SYSTEM) です。スコープの状態が異なるスレッドが同じシステムに同時に存在でき、さらに同じプロセスにも同時に存在できます。プロセススコープは、同一プロセス内の別スレッドとだけリソースの奪い合いを行うスレッドに影響を及ぼします。システムスコープは、システム内のその他のすべてのスレッドとリソースの奪い合いを行うスレッドに影響を及ぼします。ただし、Solaris 9 リリースから、これらの 2 つのスコープの差異は事実上なくなっています。

スレッド取り消し

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

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

スレッド同期

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

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

64 ビットアーキテクチャー

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

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