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

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

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

ほとんどのプログラムでは、この問題は発生しません。ほとんどのプログラムは、fork() から復帰した直後に子プロセス内で exec() を呼び出します。しかし、子プロセス内で何らかの処理を行なってから exec() を呼び出す場合、または exec() をまったく呼び出さない場合、子プロセスはデッドロックに遭遇する可能性があります。ライブラリの作成者は安全な解決策を提供してください。もっとも、fork に対して安全なライブラリを提供しなくても (このような状況が稀であるため) 大きな問題にはなりません。

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

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

しかし、forkall() では別の問題が発生する可能性があるので、使用する場合は注意が必要です。たとえば、あるスレッドが forkall() を呼び出した場合、ファイルへの入出力を実行する親スレッドが子プロセス内に複製されます。スレッドのコピーはどちらも同じファイルへの入出力を続行します。ただし、一方は親、他方は子に入出力を行うので、機能不全やファイルの破損が発生します。

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


ヒント –

Sun Studio ソフトウェアに含まれている Thread Analyzer ユーティリティーを使用すると、実行中のプログラム内のデッドロックを検出できます。詳細については、『Sun Studio 12: スレッドアナライザユーザーズガイド』を参照してください。


ライブラリ内でロックを管理するには、次の処理を行う必要があります。

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

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

Solaris スレッド、POSIX スレッドのどちらを使用する場合でも、pthread_atfork(f1, f2, f3) の呼び出しをライブラリの .init() セクションに追加できます。f1()f2()f3() の定義は次のとおりです。

f1() /* This is executed just before the process forks. */
{
 mutex_lock(L1); |
 mutex_lock(...); | -- ordered in lock order
 mutex_lock(Ln); |
 } V

f2() /* This is executed in the child after the process forks. */
 {
 mutex_unlock(L1);
 mutex_unlock(...);
 mutex_unlock(Ln);
 }

f3() /* This is executed in the parent after the process forks. */
 {
 mutex_unlock(L1);
 mutex_unlock(...);
 mutex_unlock(Ln);
 } 

仮想 fork–vfork

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

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

たとえば、vfork() を呼び出してから exec() を呼び出すまでの間に、新しいスレッドを生成すると非常に危険です。

解決策: pthread_atfork

fork 1 モデルを使用するときは必ず、pthread_atfork() を使ってデッドロックを防ぎます。

#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 ハンドラがこれらの相互排他ロックを解放することがあります。必要なすべての相互排他ロックを獲得する prepare ハンドラは、プロセスが fork される前に、関連するすべてのロックが、fork 関数を呼び出すスレッドによって保持されているようにする必要があります。この方法で、子プロセスでのデッドロックを防ぐことができます。

詳細は、pthread_atfork(3C) のマニュアルページを参照してください。