[PHP]pcntl_wait関数とは?子プロセスの終了を待機する方法を徹底解説

PHP

こんにちは!今回は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でゾンビプロセスを防ぐ方法
タイトルとURLをコピーしました