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

シグナルの拡張

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(3T) に対するシグナルの影響に注意する必要があります。この関数は、通常 pthread_cond_signal(3T)pthread_cond_broadcast(3T) に応答して戻ります。しかし、この関数で待ち状態になっているスレッドが従来の 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_sigsetmask(3T)

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

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

pthread_kill(3T)

pthread_kill(3T) は、特定のスレッドにシグナルを送るための関数で、スレッド用の 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) で説明した 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() を使用してください。

完了セマンティクス法は、例外条件の処理でよく使用されます。特に 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() が相互排他ロックを再獲得することに依存しないでください。この動作は、将来変更される可能性があります。