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

第 5 章 オペレーティング環境が関係するプログラミング

この章では、マルチスレッドと Solaris オペレーティング環境との関係について説明します。また、マルチスレッドをサポートするために Solaris オペレーティング環境に、どのような変更が加えられたかについても説明します。

プロセスの生成 − fork

Solaris オペレーティング環境における fork() 関数のデフォルト処理は、POSIX スレッドでの fork() の処理方法とはいくらか違っています。ただし、Solaris オペレーティング環境は両方の機構をサポートしています。

表 5-1 は、Solaris と pthread での fork() の処理について、相違点と類似点を示しています。POSIX スレッドまたは Solaris スレッドの側に相当するインタフェースがない項目については、「-」が記入されています。

表 5-1 POSIX と Solaris での fork() の処理の比較

 

Solaris オペレーティング環境のインタフェース 

POSIX スレッドのインタフェース 

fork1 モデル 

fork1(2)

fork(2)

汎用 fork モデル 

fork(2)

fork - 安全 

pthread_atfork(3T)

fork1 モデル

表 5-1 で示すように、pthreadfork(2) 関数の動作は、Solaris の fork1(2) 関数の動作と同じです。pthreadfork(2) 関数と Solaris の fork1(2) 関数はどちらも新しいプロセスを生成し、子プロセスに完全なアドレス空間の複製を作成しますが、スレッドについては呼び出しスレッドのみを複製します。

これは、子プロセスが生成後ただちに exec() を呼び出すような場合に利用します。実際、多くの場合に fork() を呼び出した後行われることです。この場合、子プロセスは fork() を呼び出したスレッド以外のスレッドの複製は必要としません。

子プロセスでは、fork() を呼び出してから exec() を呼び出すまでの間に、ライブラリ関数を呼び出さないようにします。ライブラリ関数の中には、fork() 呼び出し時に親の中で保持されているロックを使用するものがあるからです。子プロセスは exec() ハンドラの 1 つが呼び出されるまで、「非同期シグナル安全」操作しか行えません。

fork1 モデルにおける安全性の問題とその解決策

共有データのロックのような通常の考慮事項に加えて、次のような問題があります。実行されているスレッドが 1 つ (fork() を呼び出したスレッド) しかないときに、ライブラリは子プロセスを fork することに関して上手に処理しなくてはなりません。この場合の問題は、子プロセスの唯一のスレッドが、その子プロセスに複製されなかったスレッドによって保持されているロックを占有しようとする可能性があることです。

これは、多くのプログラムが遭遇するような問題ではありません。ほとんどのプログラムは、fork() から復帰した直後に子プロセス内で exec() を呼び出します。しかし、子プロセス内で何かの処理を行なってから exec() を呼び出す場合、または exec() をまったく呼び出さない場合、子プロセスはデッドロックに遭遇するでしょう。

ライブラリの作成者は安全な解決策を提供してください。もっとも、fork に対して安全なライブラリを提供しなくても (このような状況が稀であるため) 大きな問題にはなりません。

たとえば、T1 が何かを出力している途中で (その間、printf() のためにロックを保持している)、T2 が新しいプロセスを fork すると仮定します。この場合、子プロセス内で唯一のスレッド (T2) が printf() を呼び出せば、すぐさまデッドロックに陥ります。

POSIX の fork() と Solaris の fork1() は、それを呼び出したスレッドのみを複製します。(Solaris の fork() を呼び出せば、すべてのスレッドが複製されるので、この問題は生じません。)

デッドロックを防ぐには、fork 時にこのようなロックが保持されないようにしなければなりません。そのための最も明瞭なやり方は、fork を行うスレッドに、子プロセスによって使われる可能性のあるロックをすべて獲得させることです。printf() でそのようなことはできないので (printf()libc によって所有されているため)、fork() の呼び出しは printf() を使用していない状態で行うようにしなければなりません。

ライブラリでロックを管理するには、次の操作を実行します。

次の例では、ライブラリによって使用されるロックのリストは {L1,...Ln} で、これらのロックのロック順序も L1...Ln です。


mutex_lock(L1);
mutex_lock(L2);
fork1(...);
mutex_unlock(L1);
mutex_unlock(L2);

pthread では、pthread_atfork(f1, f2, f3) の呼び出しをライブラリの .init() セクションに追加できます。f1f2f3 の定義は次のとおりです。


f1() /* このプロセスが fork する前に実行される */
{
 mutex_lock(L1); |
 mutex_lock(...); | -- ロックの順に並べる
 mutex_lock(Ln); |
 } V

f2() /* このプロセスが fork した後に子の中で実行される */
 {
 mutex_unlock(L1);
 mutex_unlock(...);
 mutex_unlock(Ln);
 }

f3() /* このプロセスが fork した後に親の中で実行される */
 {
 mutex_unlock(L1);
 mutex_unlock(...);
 mutex_unlock(Ln);
 } 

デッドロックのもう 1 つの例として、mutex をロックした、親プロセス内のスレッド (Solaris の fork1(2) を呼び出したものではない) が考えられます。この mutex はロック状態で子プロセスにコピーされますが、その mutex をロック解除するためのスレッドはコピーされません。このため、その mutex をロックしようとする子プロセス内のスレッドは永久に待つことになります。

仮想 fork − vfork(2)

標準の vfork(2) 関数は、マルチスレッドプログラムでは危険です。vfork(2) は、呼び出しスレッドだけを子プロセスにコピーする点が fork1(2) に似ています。ただし、スレッドに対応した実装ではないので、vfork() は子プロセスにアドレス空間をコピーしません。

子プロセス内のスレッドで、exec(2) を呼び出す前にメモリーを変更しないよう十分注意してください。vfork() では、親プロセスのアドレス空間が子プロセスにそのまま渡されます。子プロセスが exec() を呼び出すか終了すると、親プロセスにアドレス空間が戻されます。したがって、子プロセスが、親プロセスの状態を変更しないようにすることが大切です。

たとえば、vfork() を呼び出してから exec() を呼び出すまでの間に、新しいスレッドを生成することは大変危険です。これが問題となるのは、fork1 モデルを使用した場合と、子プロセスが exec() の呼び出しの他にも何か行う場合だけです。ほとんどのライブラリは「fork - 安全」ではないので、pthread_atfork() を使用することによって fork に対する安全性を実装してください。

解決策 − pthread_atfork(3T)

pthread_atfork() を使用すれば、fork1 モデルを使用したときのデッドロックが防止されます。


#include <pthread.h>

int pthread_atfork(void (*prepare) (void), void (*parent) (void),
    void (*child) (void) );

pthread_atfork() 関数は、fork() を呼び出したスレッドのコンテキストで fork() の前後に呼び出される fork のハンドラを宣言します。

これらのどれでも NULL に設定できます。連続する pthread_atfork() 呼び出しの順序が重要です。

たとえば、prepare ハンドラが、必要な相互排他ロックをすべて獲得し、次に parent ハンドラと child ハンドラがそれらを解放するといった具合です。このようにすると、プロセスが fork される「前」に、関係するロックがすべて fork 関数を呼び出すスレッドによって保持されるので、子プロセスでのデッドロックが防止されます。

汎用 fork モデルを使用すれば、「fork1 モデルにおける安全性の問題とその解決策」で述べたデッドロックの問題は回避されます。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。


ENOMEM

テーブル空間が足りないので、fork ハンドラのアドレスを記録できません。

汎用 fork モデル

Solaris の fork(2) 関数は、子プロセスにアドレス空間とすべてのスレッド (および LWP) の複製を作成します。この方法を使用するのは、子プロセスで exec(2) をまったく呼び出さないが親のアドレス空間のコピーを使用するなどの場合です。汎用 fork 機能は POSIX スレッドにはありません。

なお、プロセス内のあるスレッドが Solaris の fork(2) を呼び出すと、割り込み可能なシステムコール処理中にブロックされたスレッドは EINTR を戻すので注意してください。

また、親プロセスと子プロセスの両方に保持されるロックを作成しないよう十分注意してください。このような状況が生じる可能性があるのは、ロックを共有可能なメモリー上に割り当てている (つまり、mmap()MAP_SHARED フラグを指定した) 場合です。fork 1 モデルを使用する場合、これは問題になりません。

正しい fork の選択

アプリケーションの中での fork() のセマンティクスが「汎用 fork」と「fork 1」のどちらであるかは、該当するライブラリとリンクすることによって決定されます。-lthread を指定してリンクすると、fork() のセマンティクスは「汎用 fork」になり、-lpthread を指定してリンクすると、fork() のセマンティクスは「fork 1」になります (コンパイルオプションの説明は、図 7-1 を参照してください)。

すべての fork に関係する注意事項

どの fork() 関数についても、呼び出した後で広域的状態を使用するときには注意が必要です。

たとえば、あるスレッドがファイルを逐次的に読み取っているときに同じプロセス内の別のスレッドが fork() 関数を 1 つ呼び出すと、両方のプロセスにファイル読み取り中のスレッドが存在することになります。fork() 後はファイル記述子のシークポインタが共有されるため、親プロセス内のスレッドがデータを読み取ると、子プロセス内のスレッドは残りのデータを読み取ります。この結果、連続読み取りアクセスに切れ目ができます。

プロセスの作成 − exec(2) と exit(2) について

システムコール exec(2)exit(2) の動作は、アドレス空間内のすべてのスレッドを削除する点を除いて、シングルスレッドのプロセスの場合と変わりません。どちらのシステムコールも、スレッドを含むすべての実行リソースが削除されるまでブロック状態になります。

exec() は、プロセスを再構築するときに LWP を 1 つ生成します。さらにプロセス起動時に初期スレッドを生成します。通常、初期スレッドが処理を終えると exit() を呼び出し、プロセスは削除されます。

プロセス内のすべてのスレッドが終了すると、そのプロセスも終了します。複数のスレッドをもつプロセスから exec() 関数が呼び出されると、すべてのスレッドが終了し、新しい実行可能イメージがロードされ実行されます。デストラクタ関数は呼び出されません。

タイマ、アラーム、およびプロファイル

LWP ごとのタイマ (timer_create(3R) を参照) とスレッドごとのアラーム (alarm(2) または setitimer(2) を参照) についての「サポート中止」のご案内が Solaris 2.5 リリースでされています。どちらの機能も、この節で説明するプロセスごとの代替物によって補足されています。

各 LWP は、その LWP に結合されているスレッドが使用できるリアルタイムインタバルタイマとアラームを持っています。このタイマとアラームは、一定時間が経過すると 1 つのシグナルをスレッドに送ります。

各 LWP は、その LWP に結合されているスレッドが使用できる仮想時間インタバルタイマ、またはプロファイル用のインタバルタイマも持っています。このインタバルタイマは一定時間が経過すると、それを所有している LWP に SIGVTALRM シグナルまたは SIGPROF シグナルを送ります。

LWP ごとの POSIX タイマ

Solaris 2.3 と 2.4 リリースでは、timer_create(3R) 関数が戻すタイマオブジェクトは、そのタイマ ID が呼び出し LWP の中だけで意味をもち、その期限切れシグナルが呼び出し LWP に送られるというものでした。このため、POSIX タイマ機能を使用できるスレッドは、結合スレッドに限られていました。

さらに、この制限された使用方法でも、Solaris 2.3 と 2.4 リリースのマルチスレッドアプリケーションでの POSIX タイマは、生成されるシグナルのマスキングおよび sigvent 構造体からの関連値の送信について信頼性に欠けるところがありました。

Solaris 2.5 以降のリリースでは、マクロ _POSIX_PER_PROCESS_TIMERS を定義してコンパイルされたアプリケーション、あるいはシンボル _POSIX_C_SOURCE に対して 199506L より大きな値を指定してコンパイルされたアプリケーションは、プロセスごとのタイマを作成できます。

Solaris 2.5 リリースより前のリリースでコンパイルされたアプリケーション、あるいは機能評価マクロを使わずにコンパイルされたアプリケーションは、引き続き LWP ごとの POSIX タイマを作成します。将来のリリースでは、LWP ごとのタイマを作成するための呼び出しが、プロセスごとのタイマを戻すようになる予定です。

プロセスごとのタイマのタイマ ID は、どの LWP からでも使用できます。期限切れシグナルは、特定の LWP に向けられるのではなく、そのプロセスに対して生成されます。

プロセスごとのタイマは、timer_delete(3R) の呼び出し時またはそのプロセスの終了時にのみ削除されます。

スレッドごとのアラーム

Solaris オペレーティング環境 2.3 と 2.4 リリースでは、alarm(2) または setitimer(2) の呼び出しは、呼び出し LWP の中だけで意味をもっていました。生成した LWP が終了すると、こうしたタイマは自動的に削除されました。このため、alarm()setitimer() を使用できるスレッドは、結合スレッドに限られていました。

さらに制限された使用方法でも、Solaris オペレーティング環境 2.3 と 2.4 のマルチスレッドアプリケーションでの alarm() タイマと setitimer() タイマは、これらの呼び出しを行なった結合スレッドからのシグナルのマスキングについて信頼性に欠けるところがありました。このようなマスキングが必要とされなければ、結合スレッドから出された、これら 2 つのシステムコールの動作は信頼できるものでした。

Solaris オペレーティング環境 2.5 以降のリリースでは、-lpthread (POSIX) スレッドとリンクしたアプリケーションは、alarm() を呼び出したときにプロセスごとの SIGALRM 通知を受けるようになります。alarm() で生成される SIGALRM は、特定の LWP に向けられるのではなく、そのプロセスに対して生成されます。このアラームは、そのプロセスの終了時にリセットされます。

Solaris オペレーティング環境 2.5 リリースより前のリリースでコンパイルされたアプリケーション、あるいは -lpthread とリンクされていないアプリケーションは、alarm() または setitimer() で生成されるシグナルの、LWP ごとの送信を引き続き行います。

将来のリリースでは、ITIMER_REAL フラグを指定した alarm() または setitimer() の呼び出しによって、SIGALRM がそのプロセスに送られる予定です。他のフラグについては、setitimer() で引き続き LWP ごとの送信が行われる予定です。setitimer() のフラグで ITIMER_REAL フラグ以外のものについては、生成されるシグナルが、その呼び出しを行なった LWP に送信されることに変わりはなく、したがって結合スレッドからしか使用できません。

プロファイル

profil(2) で、各 LWP に専用のバッファ、または複数の LWP で共有のバッファを用意することにより、LWP ごとにプロファイルを有効にすることが可能です。プロファイルデータの更新は、LWP ユーザ時間のクロック更新単位ごとに行われます。プロファイルの状態は、生成元の LWP から継承されます。

大域ジャンプ − setjmp(3C) と longjmp(3C)

setjmp()longjmp() の有効範囲は、1 つのスレッド内だけに制限されます。この制限は、ほとんどの場合は問題となりません。しかし、この制限は、シグナルを扱うスレッドが longjmp() を使用できるのは、setjmp() が同一スレッド内で実行されている場合だけであることを意味します。

リソースの制限

リソースの制限は、そのプロセス全体に課せられ、プロセス内のすべてのスレッドが全体でどれだけリソースを使用しているかによって決まります。リソースの弱い制限値を超えた場合は、制限に違反したスレッドにシグナルが送られます。プロセス内で使用されているリソースの合計は、getrusage(3B) で調べることができます。

LWP とスケジューリングクラス

第 1 章「マルチスレッドの基礎」の「スケジューリング」の節で説明しているように、Solaris の pthread 実装でサポートしているスケジューリング方針は SCHED_OTHER だけです。それ以外は POSIX のオプションです。

POSIX の SCHED_FIFO 方針と SCHED_RR 方針は、Solaris 標準の機構を使って複製またはエミュレートできます。この節では、これらのスケジューリング機構について説明します。

Solaris のカーネルには、プロセスのスケジューリングに関する 3 つのクラスがあります。最も優先順位が高いスケジューリングクラスは、リアルタイム (RT) クラスです。その次はシステムクラスで、ユーザプロセスには適用されません。最も低いのはタイムシェア (TS) クラスで、デフォルトのスケジューリングクラスです。

スケジューリングクラスは、LWP ごとに維持管理されます。プロセスが生成されると、そのプロセスの初期 LWP は、親プロセスのスケジューリングクラスと作成元の LWP の優先順位を継承します。その後に、非結合スレッドを実行させるために生成される LWP も、このスケジューリングクラスと優先順位を継承します。

プロセス内のすべての非結合スレッドは、同じスケジューリングクラスと優先順位が与えられます。各スケジューリングクラスは、そのクラスに設定可能な優先順位に従って、スケジューリング対象の LWP の優先順位を、全体のディスパッチ優先順位に対応付けます。

結合スレッドは、結合している LWP と同じスケジューリングクラスと優先順位をもちます。あるプロセス内の各結合スレッドは、カーネルから参照可能な固有のスケジューリングクラスと優先順位を持っています。結合スレッドは、システム内のすべての LWP との関係の中でスケジューリングされます。

スレッドの優先順位は、LWP リソースへのアクセスを調整します。デフォルトでは、LWP はタイムシェアクラスです。計算量の多いマルチスレッドの場合、スレッドの優先順位はあまり役立ちません。MT ライブラリを使って多くの同期を行うマルチスレッドアプリケーションでは、スレッドの優先順位がより意味をもちます。

スケジューリングクラスは、システムコール priocntl(2) で設定します。最初の 2 つの引数で、この設定の適用範囲を呼び出し側の LWP に限定したり、1 つ以上のプロセスのすべての LWP にしたりすることが可能です。3 番目の引数はコマンドで、次のいずれか 1 つを指定できます。

priocntl() は結合スレッドにだけ使用します。非結合スレッドの優先順位を変更する場合は、pthread_setprio(3T) を使用してください。

タイムシェアスケジューリング

タイムシェアスケジューリングでは、このスケジューリングの LWP に処理リソースが公平に配分されます。カーネルのそれ以外の部分は、ユーザに対する応答時間に悪影響を与えないようにプロセッサを短時間ずつ使用します。

システムコール priocntl(2) は、1 つ以上のプロセスの nice() レベルを設定します。priocntl() による nice() レベルの変更は、そのプロセス内のタイムシェアクラスのすべての LWP に適用されます。nice() レベルの範囲は通常は 0〜+20 で、スーパーユーザ特権をもつプロセスの場合は -20〜+20 です。この値が小さいほど優先順位が高くなります。

タイムシェアクラスの LWP をディスパッチする優先順位は、LWP のその時点での CPU 使用率と nice() レベルに基づいて計算されます。タイムシェアスケジューラにとって、nice() レベルは、LWP 間の相対的な優先順位を表します。

LWP の nice() レベルが大きいほど、その LWP に配分される CPU 時間は少なくなりますが 0 になることはありません。多くの CPU 時間をすでに消費している LWP は、CPU 時間をほとんど (あるいは、まったく) 消費していない LWP よりも優先順位が下げられます。

リアルタイムスケジューリング

リアルタイム (RT) クラスは、プロセス全体またはプロセス内の 1 つ以上の LWP に適用できます。ただし、スーパーユーザ特権が必要です。

タイムシェアクラスの nice(2) レベルとは異なり、リアルタイムクラスを指定された LWP には、個々の LWP 単位または複数の LWP 単位で優先順位を設定できます。priocntl(2) システムコールで、プロセス内のリアルタイムクラスのすべての LWP の属性を変更できます。

スケジューラは、最も高い優先順位を持つリアルタイムクラスの LWP をディスパッチします。優先順位の高い LWP が実行可能状態になると、それよりも優先順位の低い LWP は、実行リソースを横取りされます。実行リソースを横取りされた LWP は、そのレベルの待ち行列の先頭に置かれます。

リアルタイムクラスの LWP は、実行リソースが横取りされたり、一時停止したり、リアルタイム優先順位が変更されたりしない限り、プロセッサの制御を保持し続けます。リアルタイムクラスの LWP には、タイムシェアクラスのプロセスよりも絶対的に高い優先順位が与えられます。

新しく生成された LWP は、親プロセスまたは親 LWP のスケジューリングクラスを継承します。リアルタイムクラスの LWP は、親のタイムスライス (リソース割り当て時間) を有限または無限指定に関係なく継承します。

有限タイムスライスを指定された LWP は、処理が終了するか、入出力イベント待ちなどによってブロックされるか、より優先順位の高い実行可能なリアルタイムプロセスによって実行リソースを横取りされるか、またはタイムスライスが満了するまで実行を続けます。

無限タイムスライスを指定された LWP が実行を停止するのは、LWP が終了するか、ブロックされるか、または実行リソースが横取りされたときだけです。

LWP のスケジューリングとスレッドの結合

スレッドライブラリは、非結合スレッドを実行するための、実行リソース内の LWP 数を自動的に調整します。これには次の目的があります。

タイムスライスが適用されるのは LWP であって、スレッドではありません。つまり、LWP が 1 つしか存在しなければ、プロセス内でタイムスライスは行われません。その LWP 上のスレッドは、スレッド間同期機構でブロックされるか、実行リソースを横取りされるか、または終了するまで実行を続けます。

スレッドに対する優先順位は、pthread_setprio(3T) で設定できます。優先順位の低い非結合スレッドは、それよりも優先順位の高い非結合スレッドが実行可能になっていないときだけ LWP に割り当てられます。ただし、結合スレッドは自分専用の LWP をもつので、LWP を争奪することはありません。なお、pthread_setprio() で設定されるスレッド優先順位は、CPU に対してではなく LWP に対するスレッドのアクセスを調整します。

スケジューリングをきめ細かく制御する必要がある場合は、スレッドを LWP に結合します。多数の非結合スレッドが 1 つの LWP を争奪するような状況では、きめ細かい制御を実現できないからです。

特に、優先順位の低い非結合スレッドが優先順位の高い LWP 上にあり CPU 上で実行されていて、優先順位の低い LWP に割り当てられた優先順位の高い非結合スレッドが実行されていないことがあります。このようにスレッドの優先順位は、CPU へのアクセスについての 1 つのヒントにすぎません。

リアルタイムスレッドは、外部からの入力に対して迅速な応答が必要なときに使用します。たとえば、マウスの動きを追跡するスレッドは、マウスボタンのクリックにただちに反応しなければなりません。そのスレッドを LWP に結合すれば、必要なときにいつでも LWP を使用できるようになります。その LWP をリアルタイムスケジューリングクラスに割り当てれば、マウスボタンのクリックに迅速に反応するようにスケジューリングされます。

SIGWAITING − 待ち状態のスレッドのための LWP の生成

スレッドライブラリは通常、プログラムを実行するのに十分な数の LWP が実行リソース内に存在することを保証します。

プロセス内のすべての LWP が無期限の待ち状態でブロックされる (たとえば、端末またはネットワークからの読み取りがブロックされる) と、オペレーティング環境は SIGWAITING というシグナル (新たに導入されたシグナル) をプロセスに送ります。このシグナルはスレッドライブラリで処理されます。このとき、実行待ちのスレッドがプロセス内にあれば新しい LWP を生成し、適当な待ち状態のスレッドを選択して、新しい LWP に割り当てて実行します。

SIGWAITING 機構は、複数のスレッドが計算を目的としていて、もう 1 つ別のスレッドが実行可能になった場合に新しい LWP が生成されるかどうかを保証していません。計算を目的とするスレッドは、LWP の不足のために複数の実行可能なスレッドが動作するのを妨げることがあります。

thr_setconcurrency(3THR) を呼び出すことによって、これを防ぐことができます。POSIX スレッドで thr_setconcurrency() を使用すると POSIX 準拠ではなくなりますが、計算量の多い状況で非結合スレッド用の LWP が不足するのを回避するには、この使い方が望ましいでしょう。(POSIX に完全に準拠し LWP の不足も回避する唯一の方法は、PTHREAD_SCOPE_SYSTEM 結合スレッドのみを生成することです。)

thr_setconcurrency(3THR) 関数の使用方法の詳細は、「スレッドの並行度 (Solaris スレッドの場合のみ)」を参照してください。

Solaris スレッドでは、thr_create(3THR) 呼び出しで THR_NEW_LWP を使って、別の LWP を生成するという方法もあります。

LWP の存在時間

アクティブなスレッドが少なくなると、実行リソース内の一部の LWP は必要なくなります。LWP の数がアクティブなスレッドの数より多いとき、スレッドライブラリは不要な LWP を削除します。スレッドライブラリは LWP の存在時間を監視し、長い間 (現行では 5 分間) 使用されていない LWP は削除します。

シグナルの拡張

UNIX の従来のシグナルモデルが、スレッドに対しても自然な方法で使用できるように拡張されています。この拡張の主な特徴は、シグナルに対する処置がプロセス全体に適用され、シグナルマスクはスレッドごと適用されることです。プロセス全体に適用されるシグナル処置は、signal(2)sigaction(2) などの従来の機構を使って設定します。

シグナルハンドラが SIG_DFL または SIG_IGN に対して設定されている場合、シグナル (終了、コアダンプ、停止、継続、無視) を受け取ると、対象となるプロセス全体に対して指示された動作を行います。つまり、プロセス内のすべてのスレッドが対象となります。これらのシグナルでハンドラをもたないものについては、どのスレッドがシグナルを拾うかという問題は重要ではありません。これは、シグナルの受信による処置はプロセス全体に行われるからです。シグナルについては、signal(5) のマニュアルページを参照してください。

各スレッドは、スレッド専用のシグナルマスクを持っています。これによって、スレッドが使用するメモリーまたはその他の状態をシグナルハンドラも使用する限りは、スレッドは特定のシグナルをブロックできます。同じプロセス内のすべてのスレッドは、sigaction(2) またはそれに相当する機能によって設定されるシグナルハンドラを共有します。

あるプロセス内のスレッドが、別のプロセス内の特定のスレッドにシグナルを送ることはできません。kill(2) または sigsend(2) によるシグナルはプロセスに送られ、そのプロセス内のシグナルを受け取ることができるスレッドのどれか 1 つによって処理されます。

非結合スレッドは、代替シグナルスタックを使用できません。結合スレッドは、その状態が実行リソースと関連付けられているので、代替シグナルスタックを使用できます。代替シグナルスタックを使用するには、sigaction(2) を使ってシグナルを受け取れる状態にして、次に sigaltstack(2) で代替シグナルスタックを宣言して、使用可能な状態にします。

アプリケーションは、プロセス固有のシグナルハンドラを元にして、スレッド固有のシグナルハンドラを使用できます。プロセス全体のシグナルハンドラが、シグナルを処理するスレッドの識別子を、スレッド固有のシグナルハンドラのテーブルへのインデックスとして使用する方法があります。識別子 0 のスレッドは存在しません。

シグナルは、トラップや例外条件の同期シグナルと、割り込みの非同期シグナルの 2 つに大別されます。

従来の UNIX と同様、シグナルが保留状態のときに同じシグナルが再度発生しても無視されます。保留状態のシグナルは、カウンタではなく 1 ビットで表現されるからです。つまり、シグナルの転送はべき等です。

シングルスレッドのプロセスのときと同様、スレッドがシステムコールを呼び出してブロックされている間にシグナルを受け取ると、そのシステムコールは EINTR エラーを返すか、あるいはそれが入出力のシステムコールの場合には要求したバイト数が全部転送されないで戻ります。

マルチスレッドプログラムでは、特に pthread_cond_wait(3THR) に対するシグナルの影響に注意する必要があります。この関数は、通常 pthread_cond_signal(3THR)pthread_cond_broadcast(3THR) に応答して戻ります。しかし、この関数で待ち状態になっているスレッドが従来の UNIX のシグナルを受け取ると、この関数は EINTR エラーで戻ります。詳細は、「条件変数で待っているときの割り込み (Solaris スレッドのみ)」を参照してください。

同期シグナル

トラップ (SIGILLSIGFPESIGSEGV など) は、ゼロ除算を行なったり、自分に明示的にシグナルを送ったりすることによって、スレッド自体が発生させるものです。トラップは、そのトラップを発生させたスレッドだけが処理します。プロセス内の複数のスレッドが、同じ種類のトラップを同時に発生させて処理することもできます。

同期シグナルの場合には、シグナルの概念を容易に個々のスレッドに適用するように拡張できます。シグナルを処理するのが、そのシグナルを発生させたスレッド自体だからです。

ただし、そのスレッドがシグナルを処理するように準備されていない (たとえば、sigaction(2) でシグナルハンドラを設定していない) 場合は、同期シグナルを受け取るスレッドについてハンドラが起動されます。

同期シグナルは通常、スレッドだけでなく、プロセス全体に悪影響を及ぼすような重大な事態を意味するので、プロセスを終了させた方がよい場合が多くあります。

非同期シグナル

割り込み (SIGINTSIGIO など) は、あらゆるスレッドに対して、プロセス外部のなんらかの動作が原因で非同期的に発生します。非同期シグナルは、他のスレッドから明示的に送られてきたシグナルの場合も、ユーザが Control-C キーを入力したなどの外部動作を表す場合もあります。非同期シグナルの処理は、同期シグナルの場合に比べると複雑です。

割り込みは、その割り込みを受け取るようにシグナルマスクが設定されている、どのスレッドでも処理できます。複数のスレッドが、割り込みを受け取ることができるように設定されている場合は、その中の 1 つのスレッドだけが選択されます。

複数の同じシグナルがプロセスに送られた場合、スレッドがそのシグナルをマスクしていなければ、それぞれのシグナルを別のスレッドで処理できます。また、すべてのスレッドがマスクしているときは、「保留」の印が付けられ、最初にマスク解除したスレッドによって処理されます。

継続セマンティクス法

継続セマンティクス法は、従来から行われてきたシグナル処理方法です。これは、シグナルハンドラから復帰したときに割り込みが発生した時点から実行を再開する方法です。この方法は、シングルスレッドのプロセスで非同期シグナルを扱うのに適しています (詳細は、例 5-1 を参照してください)。

また、PL/1 などの一部のプログラミング言語の例外処理機構でも使用されています。


例 5-1 継続セマンティクス法


unsigned int nestcount;

unsigned int A(int i, int j) {
    nestcount++;

    if (i==0)
        return(j+1)
    else if (j==0)
        return(A(i-1, 1));
    else
        return(A(i-1, A(i, j-1)));
}

void sig(int i) {
    printf("nestcount = %d¥n", nestcount);
}

main() {
    sigset(SIGINT, sig);
    A(4,4);
}

シグナルに関する操作

pthread_sigmask(3THR)

pthread_sigmask(3THR) は、スレッドのシグナルマスクを設定するための関数です。つまり、sigprocmask(2) システムコールがプロセスに対して行うのと同じ操作をスレッドに対して行います。新しいスレッドが生成されると、その初期状態のシグナルマスクは生成元から継承されます。

マルチスレッドプロセス内で sigprocmask() を呼び出すのは、pthread_sigsetmask() を呼び出すのと同等です。詳細は、sigprocmask(2) のマニュアルページを参照してください。

pthread_kill(3THR)

pthread_kill(3THR) は、特定のスレッドにシグナルを送るための関数で、スレッド用の kill(2) と考えることができます。これは、プロセスにシグナルを送るものではありません。プロセスに送られたシグナルは、プロセス内のどのスレッドでも処理できます。pthread_kill() で送られたシグナルは、指定されたスレッドだけが処理できます。

pthread_kill() でシグナルを送ることができるのは、現在のプロセス内のスレッドに限られることに注意してください。スレッド識別子 (thread_t 型) の有効範囲が局所的であるため、現在のプロセス以外のプロセス内のスレッドを指定できないからです。

宛先のスレッドでシグナルの受信時に行われる処置 (ハンドラ、SIG_DFLSIG_IGN) は通常どおり広域的です。この意味は、たとえば、あるスレッドに SIGXXX を送信する場合、そのプロセスにとっての SIGXXX シグナル処置がそのプロセスを終了させることであれば、宛先スレッドがこのシグナルを受け取ったとき、そのプロセス全体が終了するということです。

sigwait(2)

マルチスレッドプログラムでは、sigwait(2) が好まれるインタフェースです。これは、非同期的に生成されるシグナルを非常にうまく扱えるからです。

sigwait() は、set 引数に指定したシグナルが呼び出しスレッドに送られてくるまで、そのスレッドを待ち状態にします。スレッドが待っている間は、set 引数で指定したシグナルのマスクが解除され、復帰時に元のシグナルマスクが設定し直されます。

set 引数で識別されるすべてのシグナルは、呼び出しスレッドを含むすべてのスレッドでブロックする必要があります。そうしないと、sigwait() は正確に動作しません。

非同期シグナルからプロセス内のスレッドを隔離したい場合は、sigwait() を使用します。非同期シグナルを待つスレッドを 1 つ生成しておき、他のスレッドは、現在のプロセスに送られてくる可能性のある非同期シグナルをすべてブロックするように生成します。

新しい sigwait の実装

Solaris オペレーティング環境 2.5 以降のリリースでは、Solaris オペレーティング環境 2.5 バージョンの新しい sigwait() と POSIX.1c バージョンの sigwait() の 2 種類を使用できます。新しいアプリケーションとライブラリでは、できるだけ POSIX 規格インタフェースを使用してください。Solaris オペレーティング環境バージョンは、将来のリリースではサポートされない可能性があるからです。


注 -

Solaris オペレーティング環境 2.5 バージョンの新しい sigwait() は、シグナルの無視という処置を無効にしません。以前の sigwait(2) の動作に依存しているアプリケーションは、ダミーのシグナルハンドラをインストールして、その処置を SIG_IGN からハンドラをもつように変更しない限りブレークする可能性があるため、このシグナルに対する sigwait() 呼び出しでこのシグナルは捕捉されます。


これら 2 つのバージョンの sigwait() の構文は下記のとおりです。


#include <signal.h>

/* Solaris 2.5 バージョン */
int sigwait(sigset_t *set);

/* POSIX.1c バージョン */
int sigwait(const sigset_t *set, int *sig);

指定のシグナルが送られてくると、POSIX.1c sigwait() は保留されているそのシグナルを削除し、sig にそのシグナルの番号を入れます。同時に複数のスレッドから sigwait() を呼び出すこともできますが、受け取るシグナルごとに 1 つのスレッドだけの sigwait() だけが返ってきます。

sigwait() を使うと、非同期シグナルを同期的に扱うことができます。つまり、非同期シグナルを扱うスレッドから sigwait() だけを呼び出すと、シグナルが到着しだい戻ります。sigwait() の呼び出し側も含むすべてのスレッドで非同期シグナルをマスクすることによって、非同期シグナルを特定のシグナルハンドラだけに処理させることができます。非同期シグナルを安全に処理することが可能です。

すべてのスレッドですべてのシグナルを常にマスクし、必要なときだけ sigwait() を呼び出すようにすれば、アプリケーションはシグナルに依存するスレッドに対してはるかに安全になります。

通常、sigwait() を使うときは、シグナルを待つスレッドを 1 つまたは複数生成します。sigwait() はマスクされているシグナルであっても受け取るため、それ以外のスレッドでは誤ってシグナルを受け取ることがないように、対象となるシグナルをすべてブロックしてください。

シグナルが送られてくると、スレッドは sigwait() から復帰し、シグナルを処理し、さらに次のシグナルを待ちます。このシグナル処理スレッドでは、非同期保護関数以外の関数も使用でき、他のスレッドとも通常の方法で同期をとることができます (非同期保護カテゴリについては、「マルチスレッドインタフェースの安全レベル」を参照してください)。


注 -

同期シグナルに対しては、sigwait() を決して使わないでください。


sigtimedwait(2)

sigtimedwait(2) は、指定時間が経過してもシグナルが送られてこなかったときにエラーで復帰する点を除いて、sigwait(2) と似ています。

スレッド指定シグナル

UNIX のシグナル機構が、スレッド指定という考え方で拡張されています。これは、シグナルがプロセスではなく特定のスレッドに送られるという点を除いて、通常の非同期シグナルと似ています。

独立したスレッドで非同期シグナルを待つ方が、シグナルハンドラを実装して、そこでシグナルを処理するよりも安全で簡単です。

非同期シグナルを処理するよりよい方法は、非同期シグナルを同期的に処理することです。具体的には、「sigwait(2)」で説明した sigwait(2) を呼び出すことにより、スレッドはシグナルの発生を待つことができます。


例 5-2 非同期シグナルと sigwait(2)


main() {
    sigset_t set;
    void runA(void);
    int sig;

    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    pthread_sigsetmask(SIG_BLOCK, &set, NULL);
    pthread_create(NULL, 0, runA, NULL, PTHREAD_DETACHED, NULL);

    while (1) {
        sigwait(&set, &sig);
        printf("nestcount = %d¥n", nestcount);
        printf("received signal %d¥n", sig);
    }
}

void runA() {
    A(4,4);
    exit(0);
}

この例は、例 5-1 を修正したものです。メインルーチンは SIGINT シグナルをマスクし、関数 A を呼び出す子スレッドを生成し、最後に sigwait() を呼び出して SIGINT シグナルを待ちます。

対象となるシグナルが、計算を行うためのスレッドでマスクされていることに注目してください。計算を行うためのスレッドは、メインスレッドのシグナルマスクを継承するからです。メインスレッドは SIGINT から保護されており、sigwait() の内部でだけ SIGINT に対するマスクが解除されます。

また、sigwait() を使用しているとき、システムコールから割り込まれる危険性がないことも注目してください。

完了セマンティクス法

シグナルを処理するもう 1 つの方法に、完了セマンティクス法があります。

完了セマンティクス法を使用するのは、シグナルが重大な障害が発生したことを示しているために現在のコード部を継続実行しても意味がない場合です。問題の原因となったコード部を引き続き実行する代わりに、シグナルハンドラが実行されます。つまり、シグナルハンドラによって、当該コード部の処理が完了されます。

例 5-3 で、if 文の then 部分の本体が問題のコード部です。setjmp(3C) の呼び出しは、プログラムの現在のレジスタ状態を jbuf に退避して 0 で復帰します。そして、このコード部が実行されます。


例 5-3 完了セマンティクス法


sigjmp_buf jbuf;
void mult_divide(void) {
    int a, b, c, d;
    void problem();

    sigset(SIGFPE, problem);
    while (1) {
        if (sigsetjmp(&jbuf) == 0) {
            printf("Three numbers, please:¥n");
            scanf("%d %d %d", &a, &b, &c);
            d = a*b/c;
            printf("%d*%d/%d = %d¥n", a, b, c, d);
        }
    }
}

void problem(int sig) {
    printf("Couldn't deal with them, try again¥n");
    siglongjmp(&jbuf, 1);
}

SIGFPE (浮動小数点例外条件) が発生すると、シグナルハンドラが呼び出されます。

シグナルハンドラは、siglongjmp(3C) を呼び出します。この関数は、jbuf に退避されていたレジスタ状態を復元し、プログラムを sigsetjmp() 部分から復帰させます (プログラムカウンタとスタックポインタも退避されています)。

このとき、sigsetjmp(3C)siglongjmp() の第 2 引数である 1 を返します。その結果、問題のコード部はスキップされ、while ループの次の繰り返しに入ります。

sigsetjmp(3C)siglongjmp(3C) をマルチスレッドプログラムで使うこともできますが、別のスレッドで呼び出された sigsetjmp() の結果を使って、siglongjmp() を呼び出すことはできません。

また、sigsetjmp()siglongjmp() は、シグナルマスクを退避または復元しますが、setjmp(3C)longjmp(3C) は、シグナルマスクを退避または復元しません。

シグナルハンドラでは、sigsetjmp()siglongjmp() を使用してください。

完了セマンティクス法は、例外条件の処理でよく使用されます。特に Sun AdaTM プログラミング言語では、このモデルが使用されています。


注 -

同期シグナルに対して sigwait(2) を決して使用しないでください。


シグナルハンドラと「非同期シグナル安全」

スレッドに対する安全性と似た概念に、「非同期シグナル安全」があります。「非同期シグナル安全」操作は、割り込まれている操作を妨げないことが保証されています。

「非同期シグナル安全」に関する問題が生じるのは、現在の操作がシグナルハンドラによる割り込みで動作を妨げる可能性があるときです。

たとえば、プログラムが printf(3S) を呼び出している最中にシグナルが発生し、そのシグナルを処理するハンドラ自体も printf() を呼び出すとします。その場合は、2 つの printf() 文の出力が混ざり合ってしまいます。これを避けるには、printf() がシグナルに割り込まれたときにシグナルハンドラが printf() を呼び出さないようにします。

この問題は、同期プリミティブでは解決できません。シグナルハンドラと同期対象操作の間で同期をとろうとすると、たちまちデッドロックが発生するからです。

たとえば、printf() が自分自身を相互排他ロックで保護していると仮定します。あるスレッドが printf() を呼び出している最中に、つまり相互排他ロックを保持した状態にある時に、シグナルにより割り込まれたとします。

(printf() 内部にいるスレッドから呼び出されている) ハンドラ自身が printf() を呼び出すと、すでに相互排他ロックを保持しているスレッドが、もう一度相互排他ロックを獲得しようとします。その結果、即座にデッドロックとなります。

ハンドラと操作の干渉を回避するには、そうした状況が決して発生しないようにするか (通常は危険領域でシグナルをマスクする)、シグナルハンドラ内部では「非同期シグナル安全」操作以外は使用しないようにします。

スレッドのマスクを設定することは、負荷の小さなユーザレベルの操作であるため、関数やプログラムの一部分を「非同期シグナル安全」のために修正しても負荷は大きくなりません。

POSIX が「非同期シグナル安全」を保証しているルーチンだけを表 5-2 に示します。どのようなシグナルハンドラも、これらの関数を安全に呼び出すことができます。

表 5-2 「非同期シグナル安全」関数

_exit()

fstat()

read()

sysconf()

access()

getegid()

rename()

tcdrain()

alarm()

geteuid()

rmdir()

tcflow()

cfgetispeed()

getgid()

setgid()

tcflush()

cfgetospeed()

getgroups()

setpgid()

tcgetattr()

cfsetispeed()

getpgrp()

setsid()

tcgetpgrp()

cfsetospeed()

getpid()

setuid()

tcsendbreak()

chdir()

getppid()

sigaction()

tcsetattr()

chmod()

getuid()

sigaddset()

tcsetpgrp()

chown()

kill()

sigdelset()

time()

close()

link()

sigemptyset()

times()

creat()

lseek()

sigfillset()

umask()

dup2()

mkdir()

sigismember()

uname()

dup()

mkfifo()

sigpending()

unlink()

execle()

open()

sigprocmask()

utime()

execve()

pathconf()

sigsuspend()

wait()

fcntl()

pause()

sleep()

waitpid()

fork()

pipe()

stat()

write()

条件変数で待っているときの割り込み (Solaris スレッドのみ)

スレッドが条件変数で待っている最中にシグナルが送られてきた場合の動作は、従来の規約では、割り込まれたシステムコールが EINTR エラーで戻るというものでした (ただし、プロセスは終了しないと仮定します)。

新たに注意すべき点は、cond_wait(3T) または cond_timedwait(3T) が復帰した時点で、mutex はロックし直されていることです。

Solaris では、スレッドが cond_wait() または cond_timedwait() でブロックされているとき、マスクされていないシグナルがスレッドに送られてくるとシグナルハンドラが呼び出され、その後 cond_wait() または cond_timedwait() は mutex をロックした状態で戻ります。

これは mutex がシグナルハンドラ内でロックされていることを意味します。シグナルハンドラは、スレッドの後処理をする必要があるかもしれないからです。このことは、Solaris オペレーティング環境 2.5 リリースでは成り立ちますが将来は変更される可能性があるので、この動作に依存しないでください。


注 -

POSIX スレッドでは、pthread_cond_wait(3T) はシグナルから復帰しますが、これはエラーではありません。pthread_cond_wait() は、ブロックが仮に解除されたことを示すため 0 を戻します。


例 5-4 で説明します。


例 5-4 条件変数で待っているときの割り込み


int sig_catcher() {
    sigset_t set;
    void hdlr();

    mutex_lock(&mut);

    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigsetmask(SIG_UNBLOCK, &set, 0);

    if (cond_wait(&cond, &mut) == EINTR) {
        /* シグナルが発生、ロックは保持されている */
        cleanup();
        mutex_unlock(&mut);
        return(0);
    }
    normal_processing();
    mutex_unlock(&mut);
    return(1);
}

void hdlr() {
    /* シグナルハンドラ内でロックは保持される */
    ...
}

sig_catcher() が呼び出された時点では、すべてのスレッドで SIGINT シグナルがブロックされているものとします。さらに、sigaction(2) によって hdlr()SIGINT シグナルのシグナルハンドラとして設定されているものとします。SIGINT シグナルをマスク解除して、cond_wait() で待ち状態になっているスレッドに SIGINT シグナルが送られてくると、スレッドは最初に mutex をロックし直し、その後 hdlr()を呼び出して、cond_wait() から EINTR エラーで戻ります。

sigaction()SA_RESTART フラグを指定したとしても、ここでは意味がないことに注意してください。cond_wait(3T) はシステムコールではないため、自動的に再呼び出しされないからです。cond_wa でスレッドがブロックされているときにシグナルが送られてくると、cond_wait() は常に EINTR エラーで戻ります。なお、アプリケーションでは、割り込まれた cond_wait() が相互排他ロックを再獲得することに依存しないでください。この動作は、将来変更される可能性があります。

入出力の問題

マルチスレッドプログラミングの利点の 1 つは、入出力の性能を高めることができることです。従来の UNIX の API では、この点に関してほとんどサポートされていませんでした。つまり、ファイルシステムに用意されている機構を利用するか、ファイルシステムを完全にバイパスするかのどちらかの選択肢しかありませんでした。

この節では、入出力の並行化やマルチバッファによって入出力の柔軟性を向上させるためのスレッドの使用方法を説明します。また、同期入出力 (スレッドを使用) と非同期入出力 (スレッドを使用することも使用しないこともある) の相違点と類似点についても説明します。

遠隔手続き呼び出しとしての入出力

従来の UNIX のモデルでは、入出力は同期的に行われるように見え、入出力装置に対して、あたかも遠隔手続き呼び出しを行なっているように見えました。入出力の呼び出しが復帰した時点では、入出力は完了しているか、少なくとも完了しているように見えます (たとえば、書き込み要求はオペレーティング環境内のバッファにデータを転送しただけで戻ることがあります)。

このモデルの利点は、プログラマは手続き呼び出しの考え方に馴れているので、簡単に理解できることです。

従来の UNIX システムにはなかった代替モデルに非同期モデルがあります。このモデルでは、入出力要求は操作を開始させるだけで、プログラム側がなんらかの方法で操作の完了を検出しなければなりません。

この方法は同期モデルほど簡単ではありませんが、従来のシングルスレッドの UNIX プロセスでも並行入出力などの処理が可能であるという利点があります。

非同期性の管理

非同期入出力のほとんどの機能は、マルチスレッドプログラムによる同期入出力で実現できます。具体的には、要求を出した後でその要求の完了をチェックするという非同期入出力の操作を行う代わりに、独立したスレッドで同期入出力を実行します。メインスレッド側は、pthread_join(3T) などによって入出力操作の完了を確認します。

非同期入出力

各スレッドの同期入出力で同じ効果を実現できるため、非同期入出力が必要になることはほとんどありません。ただし、スレッドで実現できない非同期入出力機能もあります。

簡単な例は、ストリームとしてテープドライブへ書き込みを行う場合です。この場合、テープに書き込まれている間はテープドライブを停止させないようにし、テープに書き込むデータをストリームとして送っている間は高速にテープを先送りします。

これを行うためにカーネル内のテープドライバは、以前のテープへの書き込み操作が完了したことを知らせる割り込みに応答する時に、待ち行列に入っている書き込み要求を発行する必要があります。

スレッドでは、書き込み順序を保証できません。スレッドの実行される順序が不定だからです。たとえば、テープに対して順番どおり書き込みを行おうとしても不可能です。

非同期入出力操作


#include <sys/asynch.h>

int aioread(int fildes, char *bufp, int bufs, off_t offset,
    int whence, aio_result_t *resultp);

int aiowrite(int filedes, const char *bufp, int bufs,
    off_t offset, int whence, aio_result_t *resultp);

aio_result_t *aiowait(const struct timeval *timeout);
int aiocancel(aio_result_t *resultp);

aioread(3)aiowrite(3) の形式は、pread(2)pwrite(2) の形式にそれぞれ似ています。違いは、引数リストの最後に引数が 1 つ追加されていることです。aioread() または aiowrite() を呼び出すと、入出力操作が開始されます (あるいは、入出力要求が待ち行列に入れられます)。

この呼び出しはブロックされずに復帰し、resultp の指す構造体に終了状態が戻されます。これは aio_result_t 型の項目で、次のフィールドで構成されています。


int aio_return;
int aio_errno;

呼び出しが失敗すると、aio_errno にエラーコードが設定されます。そうでない場合は、このフィールドには操作要求が正常に待ち行列に入れられたことを示す AIO_INPROGRESS が設定されます。

非同期入出力操作の完了は、aiowait(3) で待つことができます。この関数は、最初の aioread(3)、または aiowrite(3) で指定した aio_result_t 構造体へのポインタを返します。

この時点で aio_result_t には、read(2) または write(2) のどちらかが非同期バージョン以外で呼ばれた時と同じ情報が設定されます。この read または write が正常終了した場合、aio_return には読み書きされたバイト数が設定されます。異常終了した場合、aio_return には -1、aio_errno にはエラーコードが設定されます。

aiowait() には timeout 引数があり、呼び出し側の待ち時間を設定できます。ここに NULL ポインタを指定すれば、無期限に待つという意味になります。また、値 0 が設定されている構造体を指すポインタの場合は、まったく待たないという意味になります。

非同期入出力操作を開始し別の処理を行なって aiowait() で操作の完了を待つ、あるいは操作完了時に非同期的に送られてくる SIGIO を利用するという方法もあります。

保留状態の入出力操作を取り消すときは、aiocancel() を使用します。このルーチンを呼び出すときは、取り消そうとする非同期入出力操作の結果を格納するアドレスを引数で指定します。

共有入出力と新しい入出力システムコール

複数のスレッドが同じファイル記述子を使って同時に入出力操作を行う場合、従来の UNIX の入出力インタフェースがスレッドに対して安全ではない場合があります。この問題が生じるのは、入出力が逐次的に行われない場合です。システムコール lseek(2) でファイルオフセットを設定し、そのオフセットで次の read(2) または write(2) を呼び出してファイル内の操作開始位置を指定する場合です。このとき、同じファイル記述子に対して複数のスレッドが lseeks() を実行してしまうと矛盾が生じます。

この矛盾は、新しいシステムコール pread(2)pwrite(2) で回避できます。


#include <sys/types.h>
#include <unistd.h>

ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset);

ssize_t pwrite(int filedes, void *buf, size_t nbyte,
    off_t offset);

これらのシステムコールの動作は、ファイルオフセットを指定するための引数が追加されていることを除いて、read(2)write(2) とそれぞれ同じです。lseek(2) の代わりに、この引数でオフセットを指定すれば、複数のスレッドから同じファイル記述子に対して安全に入出力操作を実行できます。

getc(3S) と putc(3S) の代替

標準入出力に関して、もう 1 つ問題があります。getc(3S)putc(3S) などは、マクロとして実装されているため非常に高速に動作するという理由でよく使用されています。プログラムのループ内で使うときも、効率を気にする必要がないからです。

しかし、これらは、スレッドに対して安全になるよう変更されたため、以前よりも負荷が大きくなっています。変更後、(少なくとも) 2 つの内部サブルーチンが、相互排他のロックと解除のために呼び出されています。

この問題を回避するために、これらの代替マクロとして getc_unlocked(3S)putc_unlocked(3S) が提供されています。

これらの代替マクロは mutex をロックしないので、スレッドに対して安全ではないオリジナルの getc(3S)putc(3S) と同程度に高速です。

しかし、それらをスレッドに対して安全な方法で使うためには、標準入出力ストリームを保護する mutex を flockfile(3S)funlockfile(3S) で明示的にロックまたは解除しなければなりません。ループの外側を flockfile()funlockfile() で囲み、ループの内側で getc_unlocked()putc_unlocked() を呼び出します。