Windows アプリで子プロセスを安全に扱うためのチェックリスト - Job Object、終了伝播、標準入出力、watchdog のベストプラクティス
変換ツール、アップデータ、解析ワーカー、外部 CLI、PowerShell、ffmpeg、社内ユーティリティ。
Windows アプリは、思っている以上に簡単に子プロセスへ依存します。
ただ、事故るのは「起動できたかどうか」ではありません。
- 親が落ちたのに子だけ残る
- 孫プロセスだけが生き残る
stdout/stderrが詰まってWaitForExitが返らない- watchdog が監視対象と一緒に死ぬ
Kill(entireProcessTree: true)で終わったつもりが、観測だけ先に終わる
Windows で子プロセスを安全に扱うコツは、起動 API を選ぶこと ではなく、プロセス木の所有者を決め、終了手順と I/O を設計すること です。
この記事では、Job Object、終了伝播、標準入出力、watchdog を一枚の設計として整理します。
目次
- 1. まず結論
- 2. 何が危ないのか
- 3. 仕組みの役割を混ぜない
- 4. Job Object を基準にする
- 4.1 まず押さえたい 4 つ
- 1. 親終了で木ごと片づけたいなら KILL_ON_JOB_CLOSE
- 2. BREAKAWAY を軽く付けない
- 3. 起動時点から Job に入れたいなら PROC_THREAD_ATTRIBUTE_JOB_LIST
- 4. job handle の所有者を曖昧にしない
- 4.2 Job Object は observability にも使えるが、通知は万能ではない
- 4.1 まず押さえたい 4 つ
- 5. 終了伝播を protocol と timeout で設計する
- 5.1 GUI child
- 5.2 Console child
- 5.3 Worker / headless child
- 6. 標準入出力を詰まらせない
- 6.1 stdout / stderr は並列 drain
- 6.2 stdin を使うなら EOF まで設計する
- 6.3 不要な pipe end を必ず閉じる
- 6.4 UseShellExecute=false と handle 継承の扱いを曖昧にしない
- 7. watchdog は「外」に置く
- 7.1 exit 監視は wait handle ベースにする
- 7.2 UI thread で無限待機しない
- 7.3 hang watchdog には heartbeat が要る
- 7.4 再起動役は監視対象の外に置く
- 7.5 restart policy は budget で持つ
- 8. 典型パターン別の推奨構成
- 9. やってはいけないこと
- 10. まとめ
- 11. 参考資料
1. まず結論
先に、実務でいちばん効くところだけ並べます。
- 親の生死と子プロセス木の寿命を結びつけたいなら、基準点は Job Object です
- console への終了依頼 と プロセス木の回収 は別です
- 前者は process group と
GenerateConsoleCtrlEvent - 後者は Job Object
- 前者は process group と
- 起動時点から Job に入れたい なら、
STARTUPINFOEXとPROC_THREAD_ATTRIBUTE_JOB_LISTを使う設計が素直です - 標準出力 / 標準エラーは並列に吸い上げる のが基本です
stdinを使うなら、書き終えたら close して EOF を伝える ところまで設計します- watchdog は監視対象の Job の外に置く ほうが安全です
.NETのKill(entireProcessTree: true)は便利ですが、Windows の tree lifecycle 管理そのものではありません
2. 何が危ないのか
子プロセス起動の実装は、最初はだいたい 10 行前後で書けます。
でも事故るのは、その 10 行の外側です。
- 親が落ちたあと、子や孫が残り続ける
- helper がさらに helper を起動して、直下の子だけ待って満足してしまう
stdout/stderrの片側が詰まって、親も子も待ち合う- UI thread で待機して、画面も COM も固まる
- watchdog が監視対象と同じ運命共同体になっていて、異常時に一緒に落ちる
ここで大事なのは、「子プロセス管理」は 1 つの API の話ではない という点です。
少なくとも次の 4 つを分けて考えたほうが整理しやすいです。
- プロセス木を誰が所有するか
- どうやって協調終了を依頼するか
- 標準入出力をどう流すか
- 異常終了とハングをどう監視するか
3. 仕組みの役割を混ぜない
process handle / process group / Job Object は、似て見えて役割が違います。
| 仕組み | 主な役割 | 向いている場面 | それだけでは足りないこと |
|---|---|---|---|
| process handle | 1 プロセスの終了待ち、exit code 取得 | 単発ツールの完了待ち | 孫プロセスの回収 |
| process group | console への Ctrl+Break 伝播 | console child の協調終了 | 親クラッシュ時の cleanup、GUI 子プロセス |
| Job Object | プロセス木の束ね、制限、まとめて終了 | worker tree、updater、helper chain | アプリ固有の「保存してから閉じる」 |
process group は console signal をどこへ送るか を決める仕組みであって、親が死んだら木ごと片づける ための仕組みではありません。
一方で Job Object は、プロセス群を 1 単位で管理する Windows 側の仕組みです。
4. Job Object を基準にする
Job Object のいちばん強い点は、「誰の子か」ではなく「どの Job に属するか」 で process tree を束ねられることです。Job に入ったプロセスが CreateProcess で作る子は、デフォルトでその Job に入ります。
さらに、JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE を付けると、最後の job handle が閉じられたとき に Job に関連付けられた全プロセスが終了します。
4.1 まず押さえたい 4 つ
1. 親終了で木ごと片づけたいなら KILL_ON_JOB_CLOSE
これは Windows アプリで helper / worker を扱うときの土台です。TerminateJobObject を明示的に呼ぶ設計でもよいですが、親の異常終了まで含めて cleanup を親の寿命へ寄せたい なら KILL_ON_JOB_CLOSE が分かりやすいです。
2. BREAKAWAY を軽く付けない
JOB_OBJECT_LIMIT_BREAKAWAY_OK や JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK は便利そうに見えますが、cleanup できるつもりだった木から一部が抜ける 原因にもなります。意図がない限り、breakaway は付けないほうが事故率が下がります。
3. 起動時点から Job に入れたいなら PROC_THREAD_ATTRIBUTE_JOB_LIST
AssignProcessToJobObject で後から結びつけることもできます。
ただし、起動直後から Job 所属を前提にしたい 場面では、STARTUPINFOEX と PROC_THREAD_ATTRIBUTE_JOB_LIST を使って作成時に Job を指定するほうが筋がよいです。
4. job handle の所有者を曖昧にしない
KILL_ON_JOB_CLOSE は 最後の handle が閉じたとき に効きます。
つまり逆に言うと、job handle を別プロセスへ複製したり、意図せず継承させたりすると、親が死んでも想定どおり cleanup されません。誰が job handle の最終所有者か は先に決めるべきです。
4.2 Job Object は observability にも使えるが、通知は万能ではない
Job Object には I/O completion port を関連付けて通知を受ける仕組みがあります。ただし completion port の通知は、すべてのケースで完全保証される通知だと見なさないほうが安全です。
なので completion port は、
- 監視
- 集計
- ログ
- メトリクス
には便利ですが、それだけで correctness を組まない ほうがよいです。
5. 終了伝播を protocol と timeout で設計する
子プロセスの終了は、1 発の kill API で終わる話ではありません。
いちばん事故りにくいのは、だいたい次の 3 段階です。
- 協調終了を依頼する
- 短い timeout で待つ
- 最後に Job ごと強制終了する
この順番にしておくと、正常な終了経路は保ちつつ、ハング時は回収できます。
5.1 GUI child
GUI を持つ子プロセスなら、.NET では CloseMainWindow が close message 送信になります。
ただしこれは 終了要求 であって強制終了ではありません。なので、
CloseMainWindow- 一定時間待つ
- だめなら Job ごと kill
という流れにしたほうが素直です。
5.2 Console child
Console child では、GUI の close message は使えません。
このときは process group と console signal を使います。
CREATE_NEW_PROCESS_GROUP で起動し、GenerateConsoleCtrlEvent で CTRL_BREAK_EVENT を送る、という流れです。
ここで大事なのは、
CTRL_C_EVENTは特定 group への限定に向かない- signal を受け取れるのは console を共有しているプロセスだけ
CREATE_NEW_PROCESS_GROUPを使うとCTRL+Cの意味も変わる
という点です。
5.3 Worker / headless child
Worker や headless child は、GUI でも console でもないことが多いです。
この場合は、子プロセス専用の終了 protocol を持ったほうが安全です。
stdinにquitを送る- named pipe / socket / RPC で shutdown command を送る
- event object で停止要求を伝える
Windows 的には Job Object が tree cleanup を担い、アプリ的には pipe や stdin が graceful shutdown を担う、という分離が事故りにくいです。
6. 標準入出力を詰まらせない
6.1 stdout / stderr は並列 drain
最初の基本はこれです。
stdout と stderr は並列に吸い上げる。片方を全部読んでからもう片方、は詰まりやすいです。
Windows の pipe は無限バッファではありません。子が stderr を大量出力し、親が stdout しか読んでいなければ、子は write で止まり、親は終了待ちで止まる、という形が普通に起きます。
6.2 stdin を使うなら EOF まで設計する
stdin へ書けることと、子が終われることは同じではありません。
- 入力を書いたあと close しない
- 親は「もう渡した」と思っている
- 子は「まだ続きが来る」と思って待ち続ける
という状態が起きます。stdin を使うなら、書き終えたら close して EOF を伝える ところまで含めて設計する必要があります。
6.3 不要な pipe end を必ず閉じる
親側・子側の未使用 end を閉じないと、EOF が伝わらず、終了条件が崩れます。
これは単純ですが、実務ではかなり多い事故です。
6.4 UseShellExecute=false と handle 継承の扱いを曖昧にしない
標準入出力リダイレクトを使うなら、.NET では UseShellExecute=false が前提です。
Win32 でも、何を継承させるか をできるだけ絞ったほうが安全です。bInheritHandles=TRUE のまま全部継承させると、思わぬ handle leak の原因になります。
7. watchdog は「外」に置く
watchdog を入れるときに一番大事なのは、監視対象と同じ Job に入れない ことです。
worker が落ちたら再起動したいのに、その再起動役まで一緒に死んだら意味がありません。
7.1 exit 監視は wait handle ベースにする
プロセスは終了すると signaled 状態になります。
だから exit 監視は、本来 polling loop で HasExited を 100ms ごとに見る必要がありません。
Win32 なら、
WaitForSingleObjectWaitForMultipleObjectsRegisterWaitForSingleObjectSetThreadpoolWait
が正攻法です。複数 child を扱うなら、timer polling より wait handle ベースのほうが自然です。
7.2 UI thread で無限待機しない
WaitForSingleObject(INFINITE) は便利ですが、window を持つ thread で使うと message pump を止めやすいです。
UI thread、COM apartment thread、message pump を持つ thread では、待機の置き場所 を先に考えたほうが安全です。
7.3 hang watchdog には heartbeat が要る
exit watchdog は process handle で十分です。
でも hang watchdog は違います。
- CPU 100% で固まっている
- deadlock している
- event loop は生きているが進捗がない
- 入力待ちで止まっている
こういう状態は、「プロセスが生きているか」だけでは判定できません。なので hang まで見たいなら、
- heartbeat
- progress sequence
- last successful work timestamp
- health probe
のような アプリ層の生存確認 が要ります。
7.4 再起動役は監視対象の外に置く
実務でよくあるのは次の 2 パターンです。
- 親アプリが一時的に helper を起動するだけ
- 親が Job を持ち、親終了で helper tree を回収
- 長時間 worker を常駐させ、落ちたら再起動したい
- 外部の watchdog process / service が worker generation ごとに Job を作る
後者では、worker tree と restart authority を分離する ほうが設計が安定します。
7.5 restart policy は budget で持つ
watchdog を入れると、次は crash loop が始まります。
- 即再起動
- また即落ちる
- ログだけ大量に出る
これを避けるには、
- backoff
- 一定時間内の restart 回数上限
- 連続失敗時は停止して通知
という restart budget を持ったほうがよいです。
8. 典型パターン別の推奨構成
| 場面 | 推奨構成 |
|---|---|
| デスクトップアプリが単発の CLI helper を起動する | 1 起動 = 1 Job。KILL_ON_JOB_CLOSE を付け、stdout / stderr を並列 drain。キャンセル時は協調終了 → timeout → Job kill |
| helper がさらに孫プロセスを起動する | Job Object を前提にし、breakaway を許さない。起動時から固定したいなら PROC_THREAD_ATTRIBUTE_JOB_LIST |
| service / watchdog が長時間 worker tree を監視する | watchdog は外部 process / service。worker generation ごとに Job を作り、exit handle + heartbeat で監視 |
| console tool を丁寧に止めたい | CREATE_NEW_PROCESS_GROUP で起動し、CTRL_BREAK_EVENT で協調終了。その後 timeout で Job kill |
| GUI helper を閉じたい | CloseMainWindow / WM_CLOSE 相当 → timeout → Job kill |
| 多数の子プロセスを監視したい | blocking thread を増やすより RegisterWaitForSingleObject / SetThreadpoolWait を使う |
ここで一番大事なのは、graceful shutdown の仕組み と cleanup の仕組み を分けることです。
9. やってはいけないこと
Kill(entireProcessTree: true)だけで tree lifecycle が解けたと思うbInheritHandles=TRUEのまま全部継承するstdoutを全部読んでからstderrを読む- pipe の未使用 end を閉じない
- UI thread で
WaitForSingleObject(INFINITE)する - watchdog を監視対象と同じ Job に入れる
- 259 を普通の exit code に使う
- Job completion port の通知を唯一の真実にする
10. まとめ
Windows アプリで子プロセスを安全に扱うとき、いちばん効くのは次の整理です。
誰が process tree を所有するか
どうやって終了要求を伝えるか
標準入出力をどう流し切るか
watchdog をどこへ置くか
この 4 つを先に決める。
そのうえで、かなり雑に言うと次です。
- tree cleanup の基準点は Job Object
- graceful shutdown は GUI / console / worker で分ける
- stdio は並列 drain と EOF まで含めて設計する
- watchdog は監視対象の外に置き、polling ではなく wait handle と heartbeat で見る
CreateProcess や Process.Start 自体は入口にすぎません。
本当に事故率に効くのは、終了責任の所在 と I/O の流し切り です。
11. 参考資料
- Microsoft Learn, Job Objects
- Microsoft Learn, JOBOBJECT_BASIC_LIMIT_INFORMATION
- Microsoft Learn, UpdateProcThreadAttribute
- Microsoft Learn, InitializeProcThreadAttributeList
- Microsoft Learn, Inheritance (Processes and Threads)
- Microsoft Learn, CreateProcessW
- Microsoft Learn, Creating a Child Process with Redirected Input and Output
- Microsoft Learn, Pipe Handle Inheritance
- Microsoft Learn, Process.Kill
- Microsoft Learn, Process.CloseMainWindow
- Microsoft Learn, GenerateConsoleCtrlEvent
- Microsoft Learn, WaitForSingleObject
- Microsoft Learn, RegisterWaitForSingleObject
- Microsoft Learn, GetExitCodeProcess
- Microsoft Learn, JOBOBJECT_ASSOCIATE_COMPLETION_PORT
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
外部 CLI、変換ツール、worker、updater を扱う Windows アプリでは、起動方法よりもプロセス木の管理と終了設計が安定性を左右します。
不具合調査・原因解析
親が落ちたあとに子だけ残る、stdout が詰まる、watchdog ごと落ちるといった再現しにくい運用障害は、プロセス管理設計の見直しで改善しやすいです。