UNIX上でのC++ソフトウェア設計の定石 (3)

鉄則3: マルチスレッドのプログラムでのforkはやめよう


マルチスレッドのプログラムで、「自スレッド以外のスレッドが存在している状態」でfork*1を行うと、さまざまな問題を引き起こす可能性があります。「問題」の典型例としては、子プロセスのデッドロックが挙げられます。問題の詳細を把握しないまま、マルチスレッドのプログラムで不用意にforkするのはやめましょう!

何が起きるか


実例から見てみましょう。次のコードを実行すると、子プロセスは実行開始直後のdoit() 呼び出し時、高い確率でデッドロックします。

void* doit(void*) {
    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_lock(&mutex);
    struct timespec ts = {10, 0}; nanosleep(&ts, 0); // 10秒寝る
    pthread_mutex_unlock(&mutex);
    return 0;
}

int main(void) {
    pthread_t t;  pthread_create(&t, 0, doit, 0); // サブスレッド作成・起動
    if (fork() == 0) {
        // 子プロセス。
        // 子プロセスが生成される瞬間、親のサブスレッドはnanosleep中の場合が多い。
        doit(0); return 0;
    }
    pthread_join(t, 0); // サブスレッド完了待ち
}

以下にデッドロックの理由を説明いたします。


一般に、forkを行うと

  1. 親プロセスの「データ領域」は子プロセスにそのままコピー
  2. 子プロセスは、シングルスレッド状態で生成

されます。データ領域には、静的記憶域を持つ変数*2が格納されていますが、それらは子プロセスにコピーされます。また、親プロセスにスレッドが複数存在していても、子プロセスにそれらは継承されません。forkに関する上記2つの特徴がデッドロックの原因となります。


例えば次のようなシナリオを考えてみてください。上記のマルチスレッドプログラムでの不用意なforkによって子プロセスがデッドロックすることがわかると思います*3

  1. fork前の親プロセスでは、スレッド1と2が動いている
  2. スレッド1がdoit関数を呼ぶ
  3. doit関数が自身のmutexをロックする
  4. スレッド1がnanosleepを実行し、寝る
  5. ここで処理がスレッド2に切り替わる
  6. スレッド2がfork関数を呼ぶ
  7. 子プロセスが生成される。
  8. この時、子プロセスのdoit関数用mutexは「ロック状態」である。また、ロック状態を解除するスレッドは子プロセス中には存在しない!
  9. 子プロセスが処理を開始する。
  10. 子プロセスがdoit関数を呼ぶ
  11. 子プロセスがロック済みのmutexを再ロックしてしまい、デッドロックする

このdoit関数のように、マルチスレッド下でのforkで問題を引き起こす関数を、「fork-unsafeな関数」と呼ぶことがあります。逆に、問題を起こさない関数を「fork-safeな関数」と呼ぶことがあります。一部の商用UNIX*4では、OSの提供する関数について、ドキュメントにfork-safetyの記載がありますが、Linux(glibc)にはもちろん! 記載がありません。POSIXでも特に規定がありませんので、どの関数がfork-safeであるかは殆ど判別不能です。わからなければunsafeと考えるほうが良いでしょう。 (2004/9/12 追記) Wolfram Glogerさんが非同期シグナルセーフな関数を呼ぶのは規格準拠と言っておられるので調べてみたら、pthread_atforkのところに "In the meantime*5, only a short list of async-signal-safe library routines are promised to be available." とありました。そういうことのようです。


ちなみに、malloc関数は自身に固有のmutexを持っているのが通例ですので、普通はfork-unsafeです。malloc関数に依存する数多くの関数、例えばprintf関数などもfork-unsafeとなります。


いままでthread+forkは危険と書いてきましたが、一つだけ特例があります。「fork直後にすぐexecする場合は、特例として問題がない」のです。何故でしょう..?exec系関数*6が呼ばれると、プロセスの「データ領域」は一旦綺麗な状態にリセットされます。したがって、マルチスレッド状態のプロセスであっても、fork後にすぐ、危険な関数を一切呼ばずにexec関数を呼べば、子プロセスが誤動作することはないのです。ただし、「すぐ」と書いてあることに注意してください。exec前に printf(“I’m child process”); を一発呼ぶだけでもデッドロックの危険があります!

災いをどう回避するか


マルチスレッドのプログラムでのforkを安全に行うための、デッドロック問題回避の方法はあるでしょうか?いくつか考えてみます。


回避方法1: forkを行う場合は、それに先立って他スレッドを全て終了させる


forkに先立って他スレッドを全て終了させておけば、問題はおきません。ただ、それが可能なケースばかりではないでしょう。また、何らかの要因で他スレッドの終了が行われないままforkしてしまった場合、解析困難な不具合して問題が表面化してしまいます。


回避方法2: fork直後に子プロセスがexecを呼ぶようにする
(2004/9/11 書き忘れていたので追記)


回避方法1が取れない場合は、子プロセスはfork直後に、どんな関数(printfなどを含む)も呼ばずにすぐにexeclなど、execファミリーの関数を呼ぶようにします。もし、"execしないfork"を一切使わないプログラムであれば、現実的な回避方法でしょう。


回避方法3: 「他スレッド」ではfork-unsafeな処理を一切行わない


forkを呼ぶスレッドを除く全てのスレッドが、fork-unsafeな処理を一切行わない方法です。数値計算の速度向上目的でスレッドを使用している場合*7などは、なんとか可能かもしれませんが、一般のアプリケーションでは現実的ではありません。どの関数がfork-safeなのか把握することだけでも容易ではないからです。fork-safeな関数、要するに非同期シグナルセーフな関数ですが、それは数えるほどしかないからです。この方法では malloc/new, printf すら使えなくなってしまいます。


回避方法4: pthread_atfork関数を用いて、fork前後に自分で用意したコールバック関数を呼んでもらう


pthread_atfork関数を用いて、fork前後に自分で用意したコールバック関数を呼んでもらい、コールバック内で、プロセスのデータ領域を掃除する方法です。しかし、OS提供の関数(例: malloc)については、コールバック関数から掃除する方法がありません。mallocの使用するデータ構造は外部からは見えないからです。よって、pthread_atfork関数はあまり実用的ではありません。


回避方法5: マルチスレッドのプログラムでは、forkを一切使用しない


forkを一切使用しない方法です。forkするのではなく、素直にpthread_createするようにします。これも、回避策2と同様に現実的な方法であり、推奨できます。


以上。

*1:子プロセスを生成するシステムコール

*2:グローバル変数や関数内のstatic変数

*3:Linuxを使用するのであれば、pthread_atfork関数のman pageを見るとよいです。この種のシナリオについて若干の解説があります

*4:SolarisHP-UXなど

*5:fork後execするまでの間

*6:≒execveシステムコール

*7:四則演算しか行わないならfork-safe