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を行うと
- 親プロセスの「データ領域」は子プロセスにそのままコピー
- 子プロセスは、シングルスレッド状態で生成
されます。データ領域には、静的記憶域を持つ変数*2が格納されていますが、それらは子プロセスにコピーされます。また、親プロセスにスレッドが複数存在していても、子プロセスにそれらは継承されません。forkに関する上記2つの特徴がデッドロックの原因となります。
例えば次のようなシナリオを考えてみてください。上記のマルチスレッドプログラムでの不用意なforkによって子プロセスがデッドロックすることがわかると思います*3。
- fork前の親プロセスでは、スレッド1と2が動いている
- スレッド1がdoit関数を呼ぶ
- doit関数が自身のmutexをロックする
- スレッド1がnanosleepを実行し、寝る
- ここで処理がスレッド2に切り替わる
- スレッド2がfork関数を呼ぶ
- 子プロセスが生成される。
- この時、子プロセスのdoit関数用mutexは「ロック状態」である。また、ロック状態を解除するスレッドは子プロセス中には存在しない!
- 子プロセスが処理を開始する。
- 子プロセスがdoit関数を呼ぶ
- 子プロセスがロック済みの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と同様に現実的な方法であり、推奨できます。
以上。