C++で「wait関数」と言う場合、文脈によって意味が変わります。
特に代表的なのは、LinuxやmacOSなどのPOSIX環境で使われる プロセス待機用の wait() です。
これは、親プロセスが子プロセスの終了を待ち、終了状態を回収するために使われます。
ただし、厳密に言うと、この wait() はC++標準ライブラリの関数ではありません。
C++のプログラムから利用できるPOSIX系の関数です。
そのため、Windows環境や標準C++だけを前提にした環境では、そのまま使えない場合があります。
一方、C++標準ライブラリにも「wait」という名前を含む機能はあります。
たとえば、スレッド同期に使う std::condition_variable::wait() や、非同期処理の完了を待つ std::future::wait() などです。
つまり、C++でwaitについて学ぶときは、まず「何を待つためのwaitなのか」を区別することが重要です。
POSIXのwait関数の役割
子プロセスの終了を待つための関数
POSIXの wait() は、親プロセスが子プロセスの終了を待つために使います。
たとえば、親プロセスが子プロセスを作成したあと、子プロセスが終了するまで親プロセス側で待機したい場合に利用します。
wait() を呼び出すと、終了した子プロセスがすでに存在する場合はすぐに戻ります。
まだ終了した子プロセスがいない場合は、子プロセスの状態が変化するまで待機します。
基本的には、親プロセスが子プロセスを適切に管理するための関数だと考えるとわかりやすいです。
終了状態を回収する役割もある
wait() は、単に「子プロセスの終了を待つ」だけの関数ではありません。
もう一つ重要な役割として、子プロセスの終了状態を回収するという役割があります。
子プロセスが終了すると、OSはその子プロセスの終了コードなどの情報を一時的に保持します。
親プロセスがその情報を受け取るまでは、子プロセスの情報が完全には解放されません。
親プロセスが wait() を呼ぶことで、子プロセスの終了状態を取得し、OS上に残っていた情報を回収できます。
ゾンビプロセスを防ぐために重要
子プロセスが終了しているにもかかわらず、親プロセスが終了状態を回収していない状態を、一般にゾンビプロセスと呼びます。
ゾンビプロセスは、処理自体はすでに終わっています。
しかし、親プロセスが終了情報を受け取っていないため、OS上に最低限の情報が残ったままになります。
wait() や waitpid() を使って終了状態を回収すると、このゾンビ状態を解消できます。
そのため、子プロセスを作成するプログラムでは、子プロセスを作るだけでなく、終了後に適切に回収する処理も重要です。
wait関数の戻り値と状態情報
wait関数の戻り値
wait() は、正常に子プロセスを回収できた場合、終了した子プロセスのプロセスIDを返します。
一方、待つべき子プロセスが存在しない場合や、何らかのエラーが発生した場合は、エラーを表す値が返ります。
つまり、wait() を使うと、親プロセスは「どの子プロセスが終了したのか」を知ることができます。
複数の子プロセスが存在する場合、wait() は終了した子プロセスのうち、いずれか1つを回収します。
特定の子プロセスを指定して待つわけではありません。
statusには終了状態が入る
wait() では、子プロセスの終了状態を受け取ることができます。
この終了状態には、単純な終了コードだけでなく、子プロセスが正常終了したのか、シグナルによって終了したのかといった情報も含まれます。
そのため、終了状態をそのまま「終了コード」として扱うのは正しくありません。
終了状態は、専用の判定用マクロを使って確認する必要があります。
statusを直接終了コードとして扱わない
よくある誤解として、wait() で取得した状態情報をそのまま終了コードだと思ってしまうケースがあります。
しかし、status には複数の情報が詰め込まれているため、そのまま表示しても正しい終了コードとは限りません。
子プロセスが正常終了したかを確認し、そのうえで終了コードを取り出す必要があります。
この点は、wait() を学ぶうえで特に重要です。
終了状態を確認する代表的なマクロ
WIFEXITED
WIFEXITED は、子プロセスが正常終了したかどうかを判定するために使います。
ここでいう正常終了とは、プログラムが通常どおり終了した場合を指します。
たとえば、main 関数から値を返した場合や、exit によって終了した場合です。
子プロセスが正常終了していれば、終了コードを取得できます。
逆に、シグナルによって強制終了した場合などは、正常終了ではありません。
WEXITSTATUS
WEXITSTATUS は、子プロセスの終了コードを取得するために使います。
ただし、これは子プロセスが正常終了した場合にのみ使うべきです。
つまり、まず WIFEXITED で正常終了かどうかを確認し、そのあとで WEXITSTATUS によって終了コードを取り出す、という流れになります。
また、POSIX系の環境では、取得できる終了ステータスは通常、下位8ビットです。
そのため、終了コードは基本的に0〜255の範囲で扱うのが安全です。
WIFSIGNALED
WIFSIGNALED は、子プロセスがシグナルによって終了したかどうかを判定するために使います。
たとえば、強制終了された場合や、不正なメモリアクセスなどで異常終了した場合に、この判定が関係します。
正常終了ではない終了原因を調べたい場合に重要なマクロです。
WTERMSIG
WTERMSIG は、子プロセスを終了させたシグナル番号を取得するために使います。
ただし、これも WIFSIGNALED によって「シグナルで終了した」と確認できた場合に使うべきです。
正常終了したプロセスに対して、シグナル終了用の情報を取得しようとするのは適切ではありません。
wait関数とwaitpid関数の違い
waitは任意の子プロセスを待つ
wait() は、終了した任意の子プロセスを1つ回収します。
複数の子プロセスがある場合、どの子プロセスを回収するかを明示的に指定することはできません。
そのため、単純に「子プロセスのどれかが終了するのを待ちたい」という場面では便利です。
waitpidは特定の子プロセスを待てる
waitpid() は、特定の子プロセスを指定して待つことができます。
特定の処理を担当する子プロセスだけを待ちたい場合や、複数の子プロセスを細かく管理したい場合には、wait() よりも waitpid() の方が適しています。
実務的なコードでは、単純なサンプルを除き、waitpid() が使われることも多いです。
waitはwaitpidの特別な形に近い
概念的には、wait() は「任意の子プロセスを待つ waitpid()」に近い動作をします。
つまり、特定の子プロセスを指定せず、終了した子プロセスを1つ回収する処理です。
ただし、説明としては「ほぼ同じ意味」と考えるのがよく、環境や仕様の細部まで完全に同一と断言するよりも、役割が近いと理解すると安全です。
ブロッキングと非ブロッキング
waitは基本的に待機する
wait() は、回収できる子プロセスがまだ存在しない場合、子プロセスの状態が変化するまで待機します。
つまり、処理はそこで一時停止します。
このように、処理がそこで止まって待つ動作をブロッキングと呼びます。
子プロセスが終了するまで親プロセスを止めておきたい場合には、ブロッキング動作で問題ありません。
非ブロッキングで確認したい場合はwaitpidを使う
子プロセスが終了したかどうかを確認したいだけで、親プロセスの処理を止めたくない場合もあります。
その場合は、wait() ではなく waitpid() に非ブロッキング用のオプションを指定します。
非ブロッキングで確認すると、子プロセスがまだ終了していない場合でも、親プロセスは待ち続けずに処理を継続できます。
これは、サーバープログラムやイベントループを持つプログラムなどで特に重要です。
wait関数のエラー処理
子プロセスが存在しない場合
wait() は、待つべき子プロセスが存在しない場合にエラーになります。
たとえば、親プロセスが子プロセスを作成していないのに wait() を呼んだ場合や、すでにすべての子プロセスを回収済みの場合です。
このような場合は、「もう待機対象の子プロセスがいない」という意味になります。
シグナルによる中断
実務上は、wait() や waitpid() がシグナルによって中断される可能性も考慮します。
この場合、関数がエラーとして戻ってきたとしても、必ずしも致命的な失敗とは限りません。
シグナルによる一時的な中断であれば、再度 wait() または waitpid() を呼び直す設計にすることがあります。
学習段階ではまず基本動作を理解すれば十分ですが、実務コードではこのようなエラー処理も重要になります。
複数の子プロセスを扱う場合
waitは1回につき1つの子プロセスを回収する
wait() は、1回呼び出すごとに、基本的に1つの子プロセスの状態を回収します。
そのため、複数の子プロセスを作成した場合は、それぞれの終了状態を回収する必要があります。
たとえば、3つの子プロセスを作成したなら、基本的には3回分の回収処理が必要です。
作成した数だけ待つ考え方
子プロセスの数が明確に決まっている場合は、その数だけ wait() を呼ぶという考え方ができます。
これはシンプルでわかりやすい方法です。
ただし、実務では子プロセスの数が動的に変わる場合もあります。
そのような場合は、「待つべき子プロセスがいなくなるまで回収する」という考え方で処理を書くこともあります。
終了順は作成順とは限らない
複数の子プロセスがある場合、子プロセスが終了する順番は、作成した順番と一致するとは限りません。
処理内容や実行タイミングによって、あとから作成した子プロセスが先に終了することもあります。
wait() は終了した子プロセスを回収するため、「どの子が先に終わるか」は実行時の状況によって変わります。
wait関数を使うときの注意点
statusを直接使わない
wait() の状態情報を直接終了コードとして扱うのは避けるべきです。
必ず、正常終了かどうかを確認してから終了コードを取得します。
この手順を省くと、異常終了した場合やシグナルで終了した場合に、誤った解釈をしてしまう可能性があります。
子プロセス側でwaitを呼んでも意味がない場合が多い
wait() は、自分の子プロセスを待つための関数です。
そのため、親プロセスが子プロセスを待つために使うのが一般的です。
子プロセス側で wait() を呼んでも、その子プロセス自身にさらに子プロセスがいなければ、待つ対象が存在しません。
sleepで代用しない
子プロセスの終了を待つ目的で、時間待ちの処理を使うのは適切ではありません。
たとえば、「1秒待てば子プロセスは終わっているだろう」と考えて時間待ちをするのは不安定です。
処理時間は環境や負荷によって変わります。
子プロセスの終了を確実に待つなら、wait() や waitpid() を使うべきです。
終了コードは0〜255を意識する
子プロセスの終了コードは、実用上0〜255の範囲で扱うのが基本です。
大きな値を返した場合、そのまま親プロセス側で取得できるとは限りません。
成功を0、エラーを1以上の小さな値で表す設計が一般的です。
C++標準ライブラリのwait系機能
condition_variableのwait
C++標準ライブラリには、スレッド同期のための condition_variable があります。
この wait() は、プロセスではなくスレッドを待機させるための機能です。
具体的には、「ある条件が満たされるまでスレッドを待たせる」ために使います。
たとえば、別のスレッドがデータを準備するまで待つ、キューにデータが入るまで待つ、といった場面で使われます。
condition_variableでは条件付きのwaitが重要
condition_variable::wait() を使うときは、条件を指定して待つ形が非常に重要です。
理由は、スレッドが通知されたとしても、必ずしも条件が満たされているとは限らないからです。
また、通知がなくてもスレッドが起きる場合があります。これをスプリアスウェイクアップと呼びます。
そのため、スレッドが起きたあとに、もう一度条件を確認する設計が必要です。
条件付きの wait() を使うと、条件が満たされるまで内部的に確認を繰り返す形になるため、安全に待機できます。
wait中はmutexが解放される
condition_variable::wait() では、待機中にmutexが一時的に解放されます。
これは非常に重要なポイントです。
もし待機中もmutexを握り続けてしまうと、別のスレッドが条件を変更できなくなってしまいます。
実際には、wait() は待機に入るときにロックを解放し、通知などで起きたあと、処理を再開する前に再びロックを取得します。
この仕組みによって、別スレッドが条件を変更し、待機中のスレッドを起こすことができます。
futureのwait
非同期処理の完了を待つ
C++標準ライブラリには、非同期処理の結果を扱う future があります。
std::future::wait() は、その非同期処理の結果が利用可能になるまで待つための機能です。
たとえば、別スレッドで重い処理を実行し、その処理が終わるまで待ちたい場合に使います。
getも待機を兼ねる
future では、結果を取得するための get() も待機を兼ねています。
つまり、結果がまだ準備できていない状態で get() を呼ぶと、結果が用意されるまで待機します。
そのため、単に結果を取得したいだけなら、明示的に wait() を呼ばずに get() だけで十分な場合もあります。
validの状態に注意する
future を使う場合は、その future が有効な状態かどうかにも注意が必要です。
一度 get() で結果を取得すると、多くの場合、その future は有効ではなくなります。
無効な future に対して wait() や get() を呼ぶのは危険です。
そのため、future は「結果を1回受け取るもの」と考えて設計するとわかりやすいです。
threadのjoinとの違い
joinはスレッドの終了を待つ
C++でスレッドの終了を待つ場合は、通常 std::thread::join() を使います。
これは、指定したスレッドの処理が完了するまで、呼び出し元のスレッドを待機させる機能です。
POSIXの wait() が子プロセスを待つのに対し、join() はスレッドを待ちます。
waitとjoinは対象が違う
wait() と join() は、どちらも「待つ」という意味では似ています。
しかし、待つ対象が異なります。
POSIXの wait() は子プロセスを待つためのものです。
std::thread::join() はC++のスレッドを待つためのものです。
この違いを混同しないことが重要です。
sleepとの違い
sleepは時間を待つ
sleep や std::this_thread::sleep_for() は、指定した時間だけ処理を停止するために使います。
これは、「何かの終了」や「条件の成立」を待っているわけではありません。
単に、一定時間だけ処理を止めているだけです。
waitは状態を待つ
一方、wait() は子プロセスの終了など、何らかの状態変化を待つために使います。
そのため、sleep と wait は目的が異なります。
子プロセスの終了を待つなら、時間を決めて待つのではなく、終了という状態を待つべきです。
目的別の使い分け
子プロセスの終了を待ちたい場合
子プロセスの終了を待ちたい場合は、POSIX環境では wait() または waitpid() を使います。
単純にどれかの子プロセスを待ちたい場合は wait() で十分です。
特定の子プロセスを待ちたい場合は waitpid() が適しています。
特定の子プロセスを管理したい場合
複数の子プロセスを細かく管理する場合は、waitpid() を使う方が向いています。
どの子プロセスを待つのかを明示できるため、処理の流れを制御しやすくなります。
非ブロッキングで終了確認をしたい場合にも、waitpid() を使います。
スレッドの終了を待ちたい場合
C++のスレッドの終了を待つなら、std::thread::join() を使います。
プロセスではなくスレッドを扱う場合は、POSIXの wait() ではなく、C++標準ライブラリのスレッド機能を使うのが基本です。
条件が満たされるまで待ちたい場合
複数のスレッド間で、ある条件が満たされるまで待ちたい場合は、std::condition_variable::wait() を使います。
この場合は、スプリアスウェイクアップを考慮し、条件付きで待つ設計が重要です。
非同期処理の完了を待ちたい場合
std::async などで開始した非同期処理の完了を待ちたい場合は、std::future::wait() を使います。
結果を取得したい場合は、get() が待機を兼ねるため、wait() を明示的に使わなくてもよい場合があります。
一定時間だけ待ちたい場合
単に一定時間だけ処理を止めたい場合は、sleep や std::this_thread::sleep_for() を使います。
ただし、子プロセスやスレッドの終了待ちを時間待ちで代用するのは避けた方がよいです。
よくある誤解
waitはC++標準の単独関数だと思ってしまう
POSIXの wait() はC++標準ライブラリの関数ではありません。
LinuxやmacOSなどのPOSIX環境で利用できる関数です。
C++で書いたプログラムから呼び出せるため、C++の学習でも登場しますが、標準C++だけで使える機能ではない点に注意が必要です。
statusを終了コードそのものだと思ってしまう
wait() で受け取る状態情報は、終了コードそのものではありません。
正常終了したか、シグナルで終了したかなどを確認したうえで、必要な情報を取り出します。
この点を誤解すると、終了結果を正しく判定できません。
waitを1回呼べばすべての子プロセスを回収できると思ってしまう
wait() は、基本的に1回の呼び出しで1つの子プロセスを回収します。
複数の子プロセスを作成した場合は、それぞれを回収する必要があります。
子プロセスを複数作るプログラムでは、この点を忘れるとゾンビプロセスが残る可能性があります。
condition_variableのwaitは通知されれば必ず進めると思ってしまう
condition_variable::wait() は、通知されたからといって必ず条件が満たされているとは限りません。
また、通知がなくても起きる場合があります。
そのため、通知を受けたあとに条件を再確認する設計が必要です。
条件付きの wait() を使うのが基本です。
まとめ
C++で「wait関数」と言う場合、まず何を待つための機能なのかを区別する必要があります。
LinuxやmacOSなどのPOSIX環境で使われる wait() は、親プロセスが子プロセスの終了を待ち、終了状態を回収するための関数です。
子プロセスを回収することで、ゾンビプロセスを防ぐ役割もあります。
一方、C++標準ライブラリでは、スレッド同期に使う std::condition_variable::wait() や、非同期処理の完了を待つ std::future::wait() があります。
また、スレッドそのものの終了を待つ場合は std::thread::join() を使います。
それぞれの使い分けは、次のように整理できます。
| 待ちたい対象 | 使う機能 |
|---|---|
| 子プロセスの終了 | POSIXの wait() / waitpid() |
| 特定の子プロセス | waitpid() |
| スレッドの終了 | std::thread::join() |
| スレッド間の条件成立 | std::condition_variable::wait() |
| 非同期処理の完了 | std::future::wait() |
| 一定時間の待機 | sleep / std::this_thread::sleep_for() |
特にPOSIXの wait() では、終了状態を直接終了コードとして扱わず、正常終了かどうかを確認したうえで終了コードを取り出すことが重要です。
また、複数の子プロセスを作成した場合は、1回の wait() ですべてが回収されるわけではありません。
子プロセスごとに終了状態を回収する必要があります。
つまり、wait() は単なる「待機関数」ではなく、プロセス管理において子プロセスの終了を正しく扱うための重要な機能です。
以上、C++のwait関数についてでした。
最後までお読みいただき、ありがとうございました。
