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

シグナルの拡張

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

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

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

あるプロセス内のスレッドが、別のプロセス内の特定のスレッドにシグナルを送ることはできません。kill(2)、sigsend(2) 、または sigqueue (3RT) からプロセスに送信されたシグナルは、プロセス内の受け入れ可能な任意のスレッドによって処理されます。

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

従来の UNIX と同様、シグナルが保留状態のときに同じシグナルが再度発生しても通常は無視されます。保留状態のシグナルは、カウンタではなく 1 ビットで表現されるからです。しかし、sigqueue(3RT) インタフェースを使用してシグナルを送信すれば、複数の同じシグナルのインスタンスを、プロセスのキューに格納することができます。

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

マルチスレッドプログラムでは、特に pthread_cond_wait(3THR) に対するシグナルの影響に注意する必要があります。この関数は、pthread_cond_signal(3THR) または pthread_cond_broadcast(3THR) に反応した場合は、通常はエラーを返しません (戻り値が 0)。 しかし、待機中のスレッドが従来の UNIX シグナルを受信すると、誤って呼び起こされた場合でも値 0 を返します。この場合、Solaris スレッドの cond_wait (3THR) 関数は EINTR を返します。 詳細は、条件変数上で割り込まれた待機を参照してください。

同期シグナル

トラップ (SIGILLSIGFPE SIGSEGV など) は、ゼロ除算を行なったり、存在しないメモリーを参照したりすることによって、スレッド自体が発生させるものです。トラップは、そのトラップを発生させたスレッドだけが処理します。プロセス内の複数のスレッドが、同じ種類のトラップを同時に発生させて処理することもできます。

同期的に生成されたシグナルに関しては、シグナルの概念を個々のスレッドに簡単に拡張できます。シグナルハンドラは、同期シグナルを生成したスレッド上で起動されます。

しかし、適切なシグナルハンドラが設定されていない場合、そのプロセスはトラップに対応できません。その場合は、トラップが発生すると、問題を起こしたスレッドが生成されたシグナルをブロックしても、デフォルトの処理が適用されます。このようなシグナルのデフォルトの処理では、プロセスの終了に通常コアダンプを伴います。

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

非同期シグナル

割り込み (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_sigmask() を呼び出すのと同等です。詳細は、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 オペレーティング環境バージョンは、将来のリリースではサポートされない可能性があるからです。

これら 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() を再度呼び出して次のシグナルを待機します。 このシグナル処理スレッドでは、非同期シグナル安全関数以外の関数も使用でき、ほかのスレッドとも通常の方法で同期をとることができます。非同期シグナル安全カテゴリについては、マルチスレッドインタフェースの安全レベルを参照してください。


注 –

sigwait() は、同期的に生成されたシグナルを受け取ることができません。


sigtimedwait(3RT)

sigtimedwait(3RT) は、指定時間が経過してもシグナルが送られてこなかったときにエラーで復帰する点を除いて、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_sigmask(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 スレッドのインタフェース (cond_wait() cond_timedwait()) は EINTR を返します。POSIX スレッドのインタフェース (pthread_cond_wait() pthread_cond_timedwait()) は 0 を返します。 どちらのスレッドも、条件の待機状態から戻る前に、関連付けられている相互排他ロックを再度獲得します。

これは、スレッドがシグナルハンドラを実行しているときに相互排他ロックを獲得しているという意味ではありません。シグナルハンドラ内では、相互排他ロックの状態は不定です。

Solaris 9 より前のリリースに実装されている libthread では、シグナルハンドラ内の相互排他ロックの保持が保証されます。従来の動作に依存するアプリケーションは、Solaris 9 以降のリリースに合わせて修正する必要があります。

ハンドラのクリーンアップについて、例 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() の状態だとします。スレッドは hdlr() を呼び出し、cond_wait() 関数に戻ります。このとき、相互排他ロックを必要に応じて再度獲得し、cond_wait() EINTR を返します。

sigaction()SA_RESTART フラグを指定したとしても、ここでは意味がないことに注意してください。cond_wait(3THR) はシステムコールではないため、自動的に再呼び出しされないからです。cond_wa でスレッドがブロックされているときにシグナルが送られてくると、cond_wait() は常に EINTR エラーで戻ります。