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

シグナルの拡張

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

シグナルハンドラが SIG_DFL または SIG_IGN に設定されている場合、シグナルを受け取ると、受け取り側のプロセス全体に対して処理が実行されます。これらのシグナルには、終了、コアダンプ、停止、継続、無視が含まれます。これらのシグナルを受け取ったときの処理は、プロセス内のすべてのスレッドに対して実行されます。したがって、どのスレッドがシグナルを受け取るかという問題は重要ではありません。終了、コアダンプ、停止、継続、無視のシグナルは、ハンドラを持ちません。シグナルに関する基本的な情報については、signal.h(3HEAD) のマニュアルページを参照してください。

各スレッドに、固有のシグナルマスクがあります。スレッドとシグナルハンドラがメモリーまたはほかの状態を共有しているときに、スレッドはシグナルマスクに基づいて特定のシグナルをブロックします。同じプロセス内のすべてのスレッドは、sigaction(2) またはそれに相当する機能によって設定されるシグナルハンドラを共有します。

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

シグナルは、トラップ、例外、および割り込みに分類されます。 トラップと例外は、同期的に生成されるシグナルです。割り込みは、非同期的に生成されるシグナルです。

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

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

マルチスレッドプログラムでは、特に pthread_cond_wait(3C) へのシグナルの影響に注意する必要があります。この関数は通常、pthread_cond_signal(3C) または pthread_cond_broadcast(3C) への応答の場合に限り、エラー (戻り値が 0) を返さずに終了します。しかし、待機中のスレッドが従来の UNIX シグナルを受信した場合、pthread_cond_wait() は、誤って呼び起こされた場合でも値 0 を返します。

同期シグナル

SIGILLSIGFPESIGSEGV などのトラップは、スレッド上でゼロ除算や存在しないメモリーの参照などの操作が行われたときに発生します。トラップは、そのトラップを発生させたスレッドだけが処理します。プロセス内の複数のスレッドが、同じ種類のトラップを同時に発生させて処理することもできます。

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

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

こうした同期シグナルは、通常、特定のスレッドだけでなくプロセス全体に悪影響を及ぼすような重大な事態を表します。このような場合には通常、プロセスを終了するのが適切です。

非同期シグナル

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

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

同じシグナルが複数発生し、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(3C) は、sigprocmask(2) がプロセスに対して行うのと同じ処理を、スレッドに対して行います。pthread_sigmask() は、スレッドのシグナルマスクを設定します。新しいスレッドが生成されると、その初期状態のシグナルマスクは生成元から継承されます。

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

特定のスレッドへのシグナルの送信

pthread_kill(3C) は、スレッド用の kill(2) です。pthread_kill() の呼び出しは、特定のスレッドにシグナルを送ります。特定のスレッドに送られるシグナルは、プロセスに送られるシグナルとは異なります。プロセスに送られたシグナルは、プロセス内のどのスレッドでも処理できます。pthread_kill() で送られたシグナルは、指定されたスレッドだけが処理できます。

pthread_kill() でシグナルを送ることができるのは、現在のプロセス内のスレッドに限られます。スレッド識別子 (thread_t 型) のスコープは局所的であるため、現在のプロセスのスコープにないスレッドは指定できません。

宛先スレッドでシグナルの受信時に行われる処理 (ハンドラ、SIG_DFL、または SIG_IGN) は、通常どおり大域的です。あるスレッドに、あるプロセスを終了させる SIGXXX を送信する場合、宛先スレッドがこのシグナルを受け取った時点でプロセス全体が終了します。

特定のシグナルの待機

マルチスレッドプログラムでは、sigwait(2) が優先インタフェースです。これは、非同期シグナルを正しく処理できるからです。()

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

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

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

次の例は、sigwait() の構文を示しています。

#include <signal.h>
int sigwait(const sigset_t *set, int *sig
);

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

sigwait() では、非同期シグナルを同期的に扱うことができます。こうしたシグナルを扱うスレッドは、sigwait() を呼び出したあと、シグナルが到着するとすぐ終了します。sigwait() の呼び出し側を含むすべてのスレッドで非同期シグナルをマスクすることによって、非同期シグナルを特定のシグナルハンドラだけに安全に処理させることができます。

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

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

シグナルを受け取ったシグナル処理スレッドは、sigwait() から復帰してそのシグナルを処理したあと、sigwait() を再度呼び出して次のシグナルを待機します。このシグナル処理スレッドは、非同期シグナル安全関数以外にも使用できます。このシグナル処理スレッドは、ほかのスレッドとも通常の方法で同期をとることができます。非同期シグナル安全カテゴリについては、「マルチスレッドインタフェースの安全レベル」を参照してください。


注 –

sigwait() は、非同期シグナルを受け取れません。


特定のシグナルを指定された時間だけ待機

sigtimedwait(3RT)sigwait(2) に似ていますが、sigtimedwait() は指定時間内にシグナルを受け取らなかった場合に失敗してエラーを返すという点が異なっています。詳細は、sigtimedwait(3RT) のマニュアルページを参照してください。

スレッド指定シグナル

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

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

非同期シグナルを処理するよりよい方法は、非同期シグナルを同期的に処理することです。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(3C) の呼び出し中にシグナルが発生し、このシグナルのハンドラが printf() を呼び出すとします。その場合は、2 つの printf() 文の出力が混ざり合ってしまいます。この問題を避けるには、printf() がシグナルに割り込まれる可能性があるときは、シグナルハンドラが printf() を呼び出さないようにします。

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

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

このハンドラが printf() を呼び出す場合、すでに相互排他ロックを保持するスレッドが、新たに相互排他ロックを保持しようとします。その結果、即座にデッドロックが発生します。

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

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

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

_Exit()

fpathconf()

read()

sigset()

_exit()

fstat()

readlink()

sigsuspend()

abort()

fsync()

recv()

sockatmark()

accept()

ftruncate()

recvfrom()

socket()

access()

getegid()

recvmsg()

socketpair()

aio_error()

geteuid()

rename()

stat()

aio_return()

getgid()

rmdir()

symlink()

aio_suspend()

getgroups()

select()

sysconf()

alarm()

getpeername()

sem_post()

tcdrain()

bind()

getpgrp()

send()

tcflow()

cfgetispeed()

getpid()

sendmsg()

tcflush()

cfgetospeed()

getppid()

sendto()

tcgetattr()

cfsetispeed()

getsockname()

setgid()

tcgetattr()

cfsetospeed()

getsockopt()

setpgid()

tcsendbreak()

chdir()

getuid()

setsid()

tcsetattr()

chmod()

kill()

setsockopt()

tcsetpgrp()

chown()

リンク()

setuid()

time()

clock_gettime()

listen()

shutdown()

timer_getoverrun()

close()

lseek()

sigaction()

timer_gettime()

connect()

lstat()

sigaddset()

timer_settime()

creat()

mkdir()

sigdelset()

times()

dup()

mkfifo()

sigemptyset()

umask()

dup2()

open()

sigfillset()

uname()

execle()

pathconf()

sigismember()

ulink()

execve()

pause()

sleep()

utime()

fchmod()

pipe()

signal()

wait()

fchown()

poll()

sigpause()

waitpid()

fcntl()

posix_trace_event()

sigpending ()

write()

fdatasync()

pselect()

sigprocmask()

 

fork()

raise()

sigqueue()

 

条件変数上で割り込まれた待機

マスクされていない捕獲されたシグナルが条件変数上で待機しているスレッドに配信された場合、シグナルハンドラから復帰すると、スレッドは誤った呼び起こしによって条件待ち関数から復帰します。 pthread_cond_wait() および pthread_cond_timedwait() は、pthread_cond_signal() または pthread_cond_broadcast() の呼び出しが別のスレッドによって実行されなかった場合でも 0 を返します。この場合、sigaction() へのフラグとして SA_RESTART が指定されたかどうかは無視されます。pthread_cond_wait() および pthread_cond_timedwait() 関数は、自動的には再起動されません。いずれの場合も、条件待ちから復帰する前に、関連付けられている相互排他ロックを再度獲得します。

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