WaitForSingleObjectとWaitForMultipleObjectsの違いを徹底解説
Windows API を用いたマルチスレッドプログラミングでは、スレッド間の同期処理を正しく実装することが品質に直結します。Mutex や Event といったカーネルオブジェクトがシグナル状態になるのを待つ場面で頻繁に登場するのが、WaitForSingleObject と WaitForMultipleObjects です。両者は名前が似ているものの、待機対象の数や判定ロジックに大きな違いがあります。
単一の完了だけを待つ簡単なケースから、複数のスレッド完了を待ち合わせる複雑な処理まで、目的に応じて適切な関数を選択することが求められます。VC++ や C言語で Windows 向けの開発を行っている方にとって、どちらを採用するかは設計の中核を左右する判断です。
動作の仕組みを誤解したまま使うと、デッドロックや無限ループといった深刻なバグを引き起こす可能性もあります。そこで本記事では、基本仕様から戻り値、設計上の注意点まで両者の差分を体系的に整理しました。
実装例を参照しながら、現場で必要になる知見を順番に身につけていきましょう。Win32 環境における信頼性の高いスレッド同期の設計力が養われるはずです。
WaitForSingleObject の基本仕様
WaitForSingleObject は指定した単一のハンドルに対して、シグナル状態への遷移を待つシンプルな API です。Mutex、Semaphore、Event、Process、Thread など、待機可能なカーネルオブジェクトであれば何でも対象に指定できます。
引数には対象ハンドルとタイムアウト値をとり、シグナル状態になるかタイムアウト時間が経過するまでスレッドはブロックされます。タイムアウト値に INFINITE を渡すと無制限に待機し、0 を指定すると状態確認のみを行います。
待機中は CPU をほとんど消費しないため、ポーリングと比べて省電力で効率的です。コードが長くなりにくく可読性が高いのが大きな魅力ですが、複数の完了を待つ場面では別の仕組みを組み合わせる必要が出てきます。
WaitForMultipleObjects の基本仕様
WaitForMultipleObjects は複数のハンドルを同時に監視できる拡張版とも言える API です。配列に格納したハンドルのうち、すべてのオブジェクトがシグナル状態になるまで、あるいはいずれか一つがシグナル状態になるまで待機を行います。
既定の Windows では最大で MAXIMUM_WAIT_OBJECTS 個のハンドルを指定可能です。待機モードの選択には bWaitAll パラメータを用い、TRUE で全完了待ち、FALSE でいずれかの完了待ちを意味します。
FALSE モードで複数のオブジェクトが同時にシグナル化された場合は、配列の先頭に近いインデックスが優先的に返されます。そのインデックスを起点に後続の処理を分岐させることで、どのスレッドが完了したのかを一意に識別できる仕組みです。
戻り値の違いと判定方法
WaitForSingleObject の戻り値はシンプルで、WAIT_OBJECT_0 で正常待機完了、WAIT_TIMEOUT でタイムアウト、WAIT_FAILED で失敗を意味します。失敗時は GetLastError を併用すれば詳細な理由も取得可能です。
WaitForMultipleObjects では意味が二通りに分かれます。bWaitAll が TRUE のときは WAIT_OBJECT_0 で全完了を示しますが、FALSE のとき WAIT_OBJECT_0 から始まる値が「シグナル化されたインデックス」となり、数値が小さいほど優先度が高い扱いになります。
戻り値の意味を取り違えるとロジックが意図しない方向に分岐する恐れがあるため、コードを書く前に必ず仕様を確認し、コメントで意図を明記しておくのが無難な運用です。
タイムアウト処理で押さえるべきポイント
INFINITE の指定はデバッグ中には便利ですが、リリース環境で常用するのは避けるべきです。シグナルが送られない状況が続くと、該当スレッドが永久にブロックされ、プロセス全体が応答不能に陥ることがあります。
有限のタイムアウト値を設定する場合、想定される最悪ケースより少し余裕を持たせた値を選ぶのが基本です。長すぎるとユーザー体験を損ない、短すぎると正常系で誤ってタイムアウトしてしまうためシステム特性に合わせた調整が求められます。
WaitForMultipleObjects の FALSE モードでは、タイムアウト後にどのオブジェクトがシグナル化されたかを別途確認する手間が生じます。状態管理を厳密に行いたい用途では、Event オブジェクトに名前を付ける工夫も有効です。
スレッド設計での使い分け指針
単一のワーカースレッド完了を待つだけの場面では、WaitForSingleObject の簡潔さが武器になります。実装が読みやすく保守もしやすいため、小さなツールや単純な処理フローで採用する価値は高いです。
複数のスレッドが協調するバッチ処理やパイプライン型のアーキテクチャでは、WaitForMultipleObjects が適しています。ポーリングで複数の完了を検知する実装よりも CPU 使用率が下がり、コード量も短縮できます。
スレッドの本数や同期パターンをあらかじめ図に起こしておくと、どちらの API が適合するか明確になります。必要に応じて段階的に移行する戦略を取るのも現実的です。
実装時によくある落とし穴
ハンドルを API に渡す前に有効性をチェックしないと、別スレッドでクローズ済みだった場合にハングアップすることがあります。所有権の管理ルールをチーム内で統一し、ドキュメント化しておくのが安全策です。
待機対象が MAXIMUM_WAIT_OBJECTS を超える数の場合は、配列を分割して複数回 API を呼ぶか、Semaphore など別の同期機構を組み合わせる必要があります。無理に関数一つで解決しようとすると保守性が悪化します。
ファイルダイアログを開く際のスレッド制御など、より具体的な実装例を参照したい場合はファイルダイアログの実装例が参考になります。包括的な API 解説はWindows API の関連ページも併せて確認できます。
| 項目 | WaitForSingleObject | WaitForMultipleObjects |
|---|---|---|
| 待機可能オブジェクト数 | 1 個 | 最大 64 個 (既定値) |
| 待機モード | 単一判定のみ | 全完了またはいずれかを指定 |
| 戻り値の意味 | 成功・タイムアウト・失敗 | インデックス・タイムアウト・失敗 |
| ハンドルの渡し方 | 単一変数を直接渡す | 配列にまとめて渡す |
| 向いている用途 | 単一タスク完了の検知 | 複数タスクの同時同期待ち |
| 実装の手間 | 短く読みやすい | 配列操作と判定が必要 |
同期処理を安定させるための実装チェック
- 待機対象ハンドルが生きていることを API 呼び出し前に必ず確認する
- リリースビルドでは INFINITE を避け、必ず有限タイムアウト値を設定する
- bWaitAll の選択理由をソースコードコメントに明記しておく
- インデックス判定が必要な場合は安全な範囲チェックを必ず入れる
- デバッグ時は詳細な状態ログを残し、リリース時はログ量を抑える
- 複雑な依存関係はまず図にしてからコードに落とし込む
これらのポイントを押さえた上でも、特定のアプリケーション要件次第で待ち合わせる場所や粒度は大きく変わります。プログラミングの慣例として、まずは単一の待機で済む要件には WaitForSingleObject を選び、複雑さが増してきた段階で WaitForMultipleObjects へ移行するのが失敗の少ない進め方です。スレッド数と同期パターンを一度書き出して整理し、自分のプロジェクトに合致する API を選択する習慣を身につけましょう。普段書いているスレッドコードに有限タイムアウト値を導入し、ロジックの堅牢性を一段引き上げることに着手してみてください。