こんにちは!今回はPHPのマルチプロセス処理で絶対に理解しておくべきpcntl_wait()
関数について詳しく解説します。子プロセスを作成したけどゾンビプロセスになってしまう、適切なプロセス管理がしたい方は必見です!
pcntl_wait関数とは?
pcntl_wait()
は、子プロセスが終了するまで親プロセスを待機させ、終了ステータスを取得する関数です。この関数を使わないと、終了した子プロセスがゾンビプロセスとして残り続け、システムリソースを無駄に消費してしまいます。
基本構文
pcntl_wait(int &$status, int $flags = 0, array &$resource_usage = []): int
- $status: 子プロセスの終了ステータスを格納する変数(参照渡し)
- $flags: 動作フラグ(WNOHANG、WUNTRACEDなど)
- $resource_usage: リソース使用状況を格納する配列(参照渡し)
- 戻り値: 終了した子プロセスのPID、エラー時-1
なぜpcntl_wait()が必要なのか?
ゾンビプロセスの問題
// ❌ 悪い例:waitしない
$pid = pcntl_fork();
if ($pid > 0) {
// 親プロセス
echo "子プロセス作成: PID={$pid}\n";
// waitせずに終了 → 子プロセスがゾンビ化!
} elseif ($pid === 0) {
// 子プロセス
echo "子プロセス実行\n";
exit(0);
}
正しい使い方
// ✅ 良い例:waitする
$pid = pcntl_fork();
if ($pid > 0) {
// 親プロセス
echo "子プロセス作成: PID={$pid}\n";
pcntl_wait($status); // 子プロセスの終了を待つ
echo "子プロセスが終了しました\n";
} elseif ($pid === 0) {
// 子プロセス
echo "子プロセス実行\n";
exit(0);
}
pcntl_waitpidとの違い
関数 | 待機対象 | 用途 |
---|---|---|
pcntl_wait() | 任意の子プロセス1つ | 子プロセスが1つの場合や、どれか1つが終了すればよい場合 |
pcntl_waitpid() | 特定のPIDの子プロセス | 特定の子プロセスを指定して待ちたい場合 |
実践的なコード例
基本的な使い方
<?php
echo "親プロセス: PID=" . getmypid() . "\n";
$pid = pcntl_fork();
if ($pid === -1) {
die("フォーク失敗\n");
} elseif ($pid) {
// 親プロセス
echo "子プロセスを作成しました: PID={$pid}\n";
echo "子プロセスの終了を待機中...\n";
$status = 0;
$child_pid = pcntl_wait($status);
echo "子プロセス(PID:{$child_pid})が終了しました\n";
echo "終了ステータス: {$status}\n";
} else {
// 子プロセス
echo "子プロセス: PID=" . getmypid() . "\n";
echo "子プロセス: 処理実行中...\n";
sleep(3);
echo "子プロセス: 処理完了\n";
exit(0);
}
echo "親プロセス終了\n";
?>
終了ステータスの解析
<?php
function analyze_exit_status($status) {
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo "終了ステータス解析\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
if (pcntl_wifexited($status)) {
// 正常終了
$exit_code = pcntl_wexitstatus($status);
echo "✓ 正常終了\n";
echo " 終了コード: {$exit_code}\n";
} elseif (pcntl_wifsignaled($status)) {
// シグナルで終了
$signal = pcntl_wtermsig($status);
echo "✗ シグナルで終了\n";
echo " シグナル番号: {$signal}\n";
} elseif (pcntl_wifstopped($status)) {
// 停止中
$signal = pcntl_wstopsig($status);
echo "⏸ 停止中\n";
echo " 停止シグナル: {$signal}\n";
} else {
echo "? 不明な状態\n";
}
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
}
// テスト: 正常終了
$pid = pcntl_fork();
if ($pid > 0) {
pcntl_wait($status);
echo "【正常終了のケース】\n";
analyze_exit_status($status);
} elseif ($pid === 0) {
exit(42); // 終了コード42で終了
}
// テスト: エラー終了
$pid = pcntl_fork();
if ($pid > 0) {
pcntl_wait($status);
echo "\n【エラー終了のケース】\n";
analyze_exit_status($status);
} elseif ($pid === 0) {
exit(1); // 終了コード1で終了
}
?>
複数の子プロセスを待機
<?php
class MultiProcessManager {
private $children = [];
public function spawnWorkers($count = 3) {
echo "ワーカープロセスを{$count}個起動します\n\n";
for ($i = 1; $i <= $count; $i++) {
$pid = pcntl_fork();
if ($pid === -1) {
echo "フォーク失敗\n";
continue;
} elseif ($pid) {
// 親プロセス
$this->children[$pid] = $i;
echo "ワーカー{$i}起動: PID={$pid}\n";
} else {
// 子プロセス
$worker_id = $i;
$this->runWorker($worker_id);
exit(0);
}
}
echo "\n全ワーカー起動完了\n";
}
private function runWorker($id) {
echo " [ワーカー{$id}] 処理開始: PID=" . getmypid() . "\n";
// ランダムな処理時間
$sleep_time = rand(1, 5);
sleep($sleep_time);
echo " [ワーカー{$id}] 処理完了 ({$sleep_time}秒)\n";
}
public function waitAllChildren() {
echo "\n全ワーカーの終了を待機中...\n";
$completed = 0;
$total = count($this->children);
while ($completed < $total) {
$status = 0;
$pid = pcntl_wait($status);
if ($pid === -1) {
echo "エラー: 子プロセスの待機に失敗\n";
break;
}
$worker_id = $this->children[$pid] ?? '不明';
$completed++;
echo "✓ ワーカー{$worker_id} (PID:{$pid}) が終了しました ";
echo "({$completed}/{$total})\n";
if (pcntl_wifexited($status)) {
$exit_code = pcntl_wexitstatus($status);
if ($exit_code === 0) {
echo " → 正常終了\n";
} else {
echo " → 異常終了 (終了コード: {$exit_code})\n";
}
}
unset($this->children[$pid]);
}
echo "\n全ワーカーが終了しました\n";
}
}
$manager = new MultiProcessManager();
$manager->spawnWorkers(5);
$manager->waitAllChildren();
?>
リソース使用状況の取得
<?php
echo "親プロセス: PID=" . getmypid() . "\n";
$pid = pcntl_fork();
if ($pid > 0) {
// 親プロセス
echo "子プロセスの終了を待機中...\n";
$status = 0;
$rusage = [];
$child_pid = pcntl_wait($status, 0, $rusage);
echo "\n子プロセス終了: PID={$child_pid}\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo "リソース使用状況:\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
$rusage_fields = [
'ru_utime.tv_sec' => 'ユーザーCPU時間(秒)',
'ru_utime.tv_usec' => 'ユーザーCPU時間(マイクロ秒)',
'ru_stime.tv_sec' => 'システムCPU時間(秒)',
'ru_stime.tv_usec' => 'システムCPU時間(マイクロ秒)',
'ru_maxrss' => '最大メモリ使用量(KB)',
'ru_minflt' => 'マイナーページフォルト',
'ru_majflt' => 'メジャーページフォルト',
'ru_nswap' => 'スワップアウト回数',
'ru_inblock' => 'ブロック入力操作',
'ru_oublock' => 'ブロック出力操作',
'ru_nvcsw' => '自発的コンテキストスイッチ',
'ru_nivcsw' => '非自発的コンテキストスイッチ',
];
foreach ($rusage_fields as $key => $label) {
if (isset($rusage[$key])) {
echo "{$label}: {$rusage[$key]}\n";
}
}
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
} elseif ($pid === 0) {
// 子プロセス
echo "子プロセス実行中...\n";
// CPU負荷をかける
$result = 0;
for ($i = 0; $i < 1000000; $i++) {
$result += sqrt($i);
}
echo "子プロセス完了\n";
exit(0);
}
?>
WNOHANGフラグの使用(ノンブロッキング)
<?php
class NonBlockingWaiter {
private $children = [];
public function run() {
echo "親プロセス: PID=" . getmypid() . "\n";
// 子プロセスを3つ起動
for ($i = 1; $i <= 3; $i++) {
$pid = pcntl_fork();
if ($pid > 0) {
$this->children[$pid] = $i;
echo "子プロセス{$i}起動: PID={$pid}\n";
} elseif ($pid === 0) {
// 子プロセス
sleep(rand(2, 6));
exit($i);
}
}
echo "\n親プロセスの他の処理を開始します\n";
// ノンブロッキングで子プロセスをチェック
$counter = 0;
while (!empty($this->children)) {
echo "親の処理中... ({$counter})\n";
sleep(1);
$counter++;
// WNOHANGフラグ:終了した子プロセスがあれば回収、なければすぐ戻る
$status = 0;
$pid = pcntl_wait($status, WNOHANG);
if ($pid > 0) {
// 子プロセスが終了した
$worker_id = $this->children[$pid];
echo " → 子プロセス{$worker_id} (PID:{$pid}) 終了を検知\n";
unset($this->children[$pid]);
} elseif ($pid === 0) {
// まだ終了していない
// 何もしない
} else {
// エラー
echo "エラー発生\n";
break;
}
}
echo "\n全ての子プロセスが終了しました\n";
}
}
$waiter = new NonBlockingWaiter();
$waiter->run();
?>
タイムアウト付き待機
<?php
class TimeoutWaiter {
public function waitWithTimeout($timeout_seconds = 10) {
$pid = pcntl_fork();
if ($pid === -1) {
die("フォーク失敗\n");
} elseif ($pid) {
// 親プロセス
echo "子プロセス起動: PID={$pid}\n";
echo "タイムアウト: {$timeout_seconds}秒\n";
$start_time = time();
$status = 0;
while (true) {
// ノンブロッキングで待機
$result = pcntl_wait($status, WNOHANG);
if ($result > 0) {
// 子プロセスが終了
$elapsed = time() - $start_time;
echo "✓ 子プロセスが終了しました ({$elapsed}秒)\n";
return true;
} elseif ($result === -1) {
echo "エラーが発生しました\n";
return false;
}
// タイムアウトチェック
$elapsed = time() - $start_time;
if ($elapsed >= $timeout_seconds) {
echo "✗ タイムアウトしました ({$elapsed}秒)\n";
echo "子プロセスを強制終了します\n";
posix_kill($pid, SIGTERM);
sleep(1);
// まだ終了していなければSIGKILL
$result = pcntl_wait($status, WNOHANG);
if ($result === 0) {
posix_kill($pid, SIGKILL);
pcntl_wait($status);
}
return false;
}
// 短時間待機
usleep(100000); // 0.1秒
}
} else {
// 子プロセス
echo "子プロセス: 長い処理を実行中...\n";
sleep(15); // 意図的に長い処理
echo "子プロセス: 完了\n";
exit(0);
}
}
}
$waiter = new TimeoutWaiter();
$waiter->waitWithTimeout(5);
?>
エラーハンドリング
<?php
class SafeProcessWaiter {
public function waitForChild() {
$pid = pcntl_fork();
if ($pid === -1) {
throw new RuntimeException("フォークに失敗しました");
}
if ($pid) {
// 親プロセス
return $this->safeWait($pid);
} else {
// 子プロセス
sleep(2);
exit(0);
}
}
private function safeWait($expected_pid) {
$status = 0;
$result_pid = pcntl_wait($status);
if ($result_pid === -1) {
// エラー発生
$errno = pcntl_get_last_error();
$errmsg = pcntl_strerror($errno);
throw new RuntimeException(
"pcntl_wait失敗: {$errmsg} (errno: {$errno})"
);
}
if ($result_pid !== $expected_pid) {
throw new RuntimeException(
"予期しないPID: 期待={$expected_pid}, 実際={$result_pid}"
);
}
echo "子プロセス(PID:{$result_pid})が正常に終了しました\n";
return [
'pid' => $result_pid,
'status' => $status,
'exit_code' => pcntl_wexitstatus($status)
];
}
}
try {
$waiter = new SafeProcessWaiter();
$result = $waiter->waitForChild();
echo "終了情報:\n";
print_r($result);
} catch (RuntimeException $e) {
echo "エラー: " . $e->getMessage() . "\n";
}
?>
ワーカープール実装
<?php
class WorkerPool {
private $max_workers;
private $active_workers = [];
private $task_queue = [];
public function __construct($max_workers = 4) {
$this->max_workers = $max_workers;
}
public function addTask($task_id) {
$this->task_queue[] = $task_id;
}
public function run() {
echo "ワーカープール起動 (最大ワーカー数: {$this->max_workers})\n";
echo "タスク数: " . count($this->task_queue) . "\n\n";
while (!empty($this->task_queue) || !empty($this->active_workers)) {
// 空きがあればワーカーを起動
while (count($this->active_workers) < $this->max_workers && !empty($this->task_queue)) {
$task = array_shift($this->task_queue);
$this->spawnWorker($task);
}
// 終了したワーカーを回収
if (!empty($this->active_workers)) {
$status = 0;
$pid = pcntl_wait($status, WNOHANG);
if ($pid > 0) {
$task_id = $this->active_workers[$pid];
echo "✓ タスク{$task_id}完了 (PID:{$pid})\n";
echo " アクティブワーカー: " . count($this->active_workers) . "/{$this->max_workers}\n";
echo " 残タスク: " . count($this->task_queue) . "\n\n";
unset($this->active_workers[$pid]);
}
}
usleep(100000); // 0.1秒待機
}
echo "全タスク完了\n";
}
private function spawnWorker($task_id) {
$pid = pcntl_fork();
if ($pid > 0) {
// 親プロセス
$this->active_workers[$pid] = $task_id;
echo "→ タスク{$task_id}開始: PID={$pid}\n";
} elseif ($pid === 0) {
// 子プロセス
$this->executeTask($task_id);
exit(0);
}
}
private function executeTask($task_id) {
// タスク実行(ランダム時間)
$duration = rand(1, 3);
sleep($duration);
}
}
// 使用例
$pool = new WorkerPool(3);
// タスクを追加
for ($i = 1; $i <= 10; $i++) {
$pool->addTask($i);
}
$pool->run();
?>
重要なポイントと注意事項
⚠️ 必ずwaitを呼ぶ
子プロセスを作成したら必ずwaitで回収しましょう:
// ❌ ゾンビプロセスが発生
$pid = pcntl_fork();
if ($pid === 0) exit(0);
// waitしないで親が終了 → ゾンビ化
// ✅ 正しい
$pid = pcntl_fork();
if ($pid > 0) {
pcntl_wait($status);
} elseif ($pid === 0) {
exit(0);
}
💡 複数の子プロセスの場合
複数の子プロセスを待つ場合は、回数分waitを呼びます:
$children = [];
// 5つの子プロセスを起動
for ($i = 0; $i < 5; $i++) {
$pid = pcntl_fork();
if ($pid > 0) {
$children[] = $pid;
} elseif ($pid === 0) {
exit(0);
}
}
// 全て回収
for ($i = 0; $i < count($children); $i++) {
pcntl_wait($status);
}
🔍 WNOHANGの活用
ブロッキングしたくない場合はWNOHANGフラグを使います:
// ブロッキング(子プロセスが終了するまで待つ)
$pid = pcntl_wait($status);
// ノンブロッキング(すぐに戻る)
$pid = pcntl_wait($status, WNOHANG);
if ($pid === 0) {
echo "まだ終了していない\n";
}
ベストプラクティス
1. 終了ステータスを必ず確認
pcntl_wait($status);
if (pcntl_wifexited($status)) {
$exit_code = pcntl_wexitstatus($status);
if ($exit_code !== 0) {
echo "子プロセスがエラー終了: {$exit_code}\n";
}
}
2. エラーハンドリングを実装
$pid = pcntl_wait($status);
if ($pid === -1) {
$errno = pcntl_get_last_error();
error_log("pcntl_wait failed: " . pcntl_strerror($errno));
}
3. SIGCHLDシグナルの活用
pcntl_signal(SIGCHLD, function() {
while (($pid = pcntl_wait($status, WNOHANG)) > 0) {
echo "子プロセス{$pid}を回収\n";
}
});
まとめ
pcntl_wait()
は、マルチプロセスプログラミングの基本となる必須関数です。重要なポイント:
- ✅ 子プロセスの終了を待機してゾンビ化を防ぐ
- ✅ 終了ステータスを取得して処理結果を確認
- ✅ WNOHANGでノンブロッキング動作が可能
- ✅ リソース使用状況も取得できる
- ✅ 複数の子プロセスを順次回収できる
子プロセスを作成したら必ずpcntl_wait()で回収する習慣をつけましょう。これにより、堅牢でリソース効率の良いマルチプロセスアプリケーションを実現できます!
関連記事
- pcntl_waitpid()で特定のプロセスを待機
- pcntl_fork()で子プロセスを作成
- PHPでゾンビプロセスを防ぐ方法