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

PHP

こんにちは!今回はPHPのマルチプロセス処理でより細かい制御が必要な時に使うpcntl_waitpid()関数について詳しく解説します。特定の子プロセスだけを待ちたい、複数の子プロセスを柔軟に管理したい方は必見です!

pcntl_waitpid関数とは?

pcntl_waitpid()は、指定したPIDの子プロセスの終了を待機する関数です。pcntl_wait()が「任意の子プロセス」を待つのに対し、pcntl_waitpid()特定の子プロセスを指定して待つことができます。

基本構文

pcntl_waitpid(int $process_id, int &$status, int $flags = 0, array &$resource_usage = []): int
  • $process_id: 待機する子プロセスのPID(特殊値:-1、0、負の値も可能)
  • $status: 終了ステータスを格納する変数(参照渡し)
  • $flags: 動作フラグ(WNOHANG、WUNTRACEDなど)
  • $resource_usage: リソース使用状況を格納する配列(参照渡し)
  • 戻り値: 終了した子プロセスのPID、待機中は0、エラー時-1

pcntl_waitとの違い

関数PID指定用途
pcntl_wait()不可(任意の子プロセス)シンプルな待機
pcntl_waitpid()可能(特定のPID指定)複数プロセスの柔軟な管理
// pcntl_wait: どの子プロセスが終了してもOK
$pid = pcntl_wait($status);

// pcntl_waitpid: 特定のPIDを指定
$pid = pcntl_waitpid($specific_pid, $status);

process_idパラメータの特殊な値

意味
> 0指定したPIDの子プロセスを待つ
-1任意の子プロセスを待つ(pcntl_wait()と同じ)
0呼び出しプロセスと同じプロセスグループの子プロセスを待つ
< -1プロセスグループID = abs($process_id)の子プロセスを待つ

実践的なコード例

基本的な使い方

<?php
echo "親プロセス: PID=" . getmypid() . "\n\n";

// 子プロセスを作成
$pid = pcntl_fork();

if ($pid === -1) {
    die("フォーク失敗\n");
    
} elseif ($pid) {
    // 親プロセス
    echo "子プロセスを作成: PID={$pid}\n";
    echo "子プロセス{$pid}の終了を待機中...\n";
    
    // 特定のPIDを指定して待機
    $status = 0;
    $result = pcntl_waitpid($pid, $status);
    
    if ($result === $pid) {
        echo "子プロセス{$pid}が終了しました\n";
        
        if (pcntl_wifexited($status)) {
            $exit_code = pcntl_wexitstatus($status);
            echo "終了コード: {$exit_code}\n";
        }
    } elseif ($result === -1) {
        echo "エラーが発生しました\n";
    }
    
} else {
    // 子プロセス
    echo "子プロセス実行中: PID=" . getmypid() . "\n";
    sleep(3);
    echo "子プロセス完了\n";
    exit(42);
}

echo "\n親プロセス終了\n";
?>

複数の子プロセスを個別に管理

<?php
class ProcessManager {
    private $processes = [];
    
    public function spawn($worker_id, $duration) {
        $pid = pcntl_fork();
        
        if ($pid === -1) {
            throw new RuntimeException("フォーク失敗");
            
        } elseif ($pid) {
            // 親プロセス
            $this->processes[$pid] = [
                'id' => $worker_id,
                'start_time' => time(),
                'duration' => $duration
            ];
            
            echo "ワーカー{$worker_id}起動: PID={$pid}, 予定時間={$duration}秒\n";
            return $pid;
            
        } else {
            // 子プロセス
            sleep($duration);
            exit(0);
        }
    }
    
    public function waitForSpecificProcess($pid) {
        if (!isset($this->processes[$pid])) {
            echo "PID {$pid} は管理されていません\n";
            return false;
        }
        
        $worker = $this->processes[$pid];
        echo "\nワーカー{$worker['id']} (PID:{$pid})を待機中...\n";
        
        $status = 0;
        $result = pcntl_waitpid($pid, $status);
        
        if ($result === $pid) {
            $elapsed = time() - $worker['start_time'];
            echo "✓ ワーカー{$worker['id']}が終了しました (実行時間: {$elapsed}秒)\n";
            
            unset($this->processes[$pid]);
            return true;
        }
        
        return false;
    }
    
    public function listRunningProcesses() {
        echo "\n実行中のプロセス:\n";
        foreach ($this->processes as $pid => $info) {
            $elapsed = time() - $info['start_time'];
            echo "  PID {$pid}: ワーカー{$info['id']} (経過: {$elapsed}秒)\n";
        }
    }
}

$manager = new ProcessManager();

// 異なる実行時間の子プロセスを起動
$pid1 = $manager->spawn(1, 5);
$pid2 = $manager->spawn(2, 3);
$pid3 = $manager->spawn(3, 7);

sleep(1);
$manager->listRunningProcesses();

// 特定のプロセスを順番に待つ
echo "\n=== 最初にPID {$pid2}を待ちます ===\n";
$manager->waitForSpecificProcess($pid2);

sleep(1);
$manager->listRunningProcesses();

echo "\n=== 次にPID {$pid1}を待ちます ===\n";
$manager->waitForSpecificProcess($pid1);

echo "\n=== 最後にPID {$pid3}を待ちます ===\n";
$manager->waitForSpecificProcess($pid3);

echo "\n全てのプロセスが終了しました\n";
?>

WNOHANGフラグ:ノンブロッキング待機

<?php
class NonBlockingManager {
    private $children = [];
    
    public function run() {
        echo "親プロセス: PID=" . getmypid() . "\n\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_time = rand(2, 6);
                echo "  [子{$i}] {$sleep_time}秒処理します\n";
                sleep($sleep_time);
                exit($i);
            }
        }
        
        echo "\n親プロセスの他の処理を開始\n";
        
        // 各子プロセスをノンブロッキングで監視
        $counter = 0;
        while (!empty($this->children)) {
            echo "親の処理: カウント={$counter}\n";
            sleep(1);
            $counter++;
            
            // 各子プロセスをチェック
            foreach (array_keys($this->children) as $pid) {
                $status = 0;
                $result = pcntl_waitpid($pid, $status, WNOHANG);
                
                if ($result === $pid) {
                    // このプロセスが終了した
                    $worker_id = $this->children[$pid];
                    $exit_code = pcntl_wexitstatus($status);
                    
                    echo "  → 子プロセス{$worker_id} (PID:{$pid}) 終了 (code:{$exit_code})\n";
                    unset($this->children[$pid]);
                    
                } elseif ($result === 0) {
                    // まだ実行中
                    // 何もしない
                    
                } elseif ($result === -1) {
                    echo "  → エラー: PID {$pid}\n";
                    unset($this->children[$pid]);
                }
            }
        }
        
        echo "\n全ての子プロセスが終了しました\n";
    }
}

$manager = new NonBlockingManager();
$manager->run();
?>

process_id = -1の使用例

<?php
echo "親プロセス: PID=" . getmypid() . "\n";

$children = [];

// 5つの子プロセスを起動
for ($i = 1; $i <= 5; $i++) {
    $pid = pcntl_fork();
    
    if ($pid > 0) {
        $children[$pid] = $i;
        echo "子プロセス{$i}起動: PID={$pid}\n";
        
    } elseif ($pid === 0) {
        // 子プロセス
        sleep(rand(1, 3));
        exit($i);
    }
}

echo "\n任意の子プロセスが終了する順に回収します\n";

// process_id = -1 で任意の子プロセスを待つ
$completed = 0;
while ($completed < count($children)) {
    $status = 0;
    $pid = pcntl_waitpid(-1, $status); // -1 = 任意の子プロセス
    
    if ($pid > 0) {
        $worker_id = $children[$pid];
        $exit_code = pcntl_wexitstatus($status);
        $completed++;
        
        echo "✓ 子プロセス{$worker_id} (PID:{$pid}) 終了 ";
        echo "({$completed}/" . count($children) . ")\n";
    }
}

echo "\n全て終了\n";
?>

優先度付き待機

<?php
class PriorityWaiter {
    private $children = [];
    
    public function run() {
        echo "親プロセス: PID=" . getmypid() . "\n";
        
        // 異なる優先度のタスクを起動
        $priorities = [
            'high' => [],
            'normal' => [],
            'low' => []
        ];
        
        // 高優先度タスク
        for ($i = 1; $i <= 2; $i++) {
            $pid = $this->spawnTask("高優先度{$i}", 3);
            $priorities['high'][$pid] = "高優先度{$i}";
        }
        
        // 通常優先度タスク
        for ($i = 1; $i <= 3; $i++) {
            $pid = $this->spawnTask("通常{$i}", 2);
            $priorities['normal'][$pid] = "通常{$i}";
        }
        
        // 低優先度タスク
        for ($i = 1; $i <= 2; $i++) {
            $pid = $this->spawnTask("低優先度{$i}", 1);
            $priorities['low'][$pid] = "低優先度{$i}";
        }
        
        echo "\n優先度順に回収します\n";
        
        // 高優先度から順に待機
        foreach (['high', 'normal', 'low'] as $priority) {
            echo "\n【{$priority}の回収】\n";
            
            foreach (array_keys($priorities[$priority]) as $pid) {
                $status = 0;
                $result = pcntl_waitpid($pid, $status);
                
                if ($result === $pid) {
                    $task_name = $priorities[$priority][$pid];
                    echo "  ✓ {$task_name} (PID:{$pid}) 完了\n";
                }
            }
        }
        
        echo "\n全タスク完了\n";
    }
    
    private function spawnTask($name, $duration) {
        $pid = pcntl_fork();
        
        if ($pid > 0) {
            echo "タスク起動: {$name} (PID:{$pid})\n";
            return $pid;
            
        } elseif ($pid === 0) {
            sleep($duration);
            exit(0);
        }
        
        return -1;
    }
}

$waiter = new PriorityWaiter();
$waiter->run();
?>

タイムアウト付き特定プロセス待機

<?php
class TimedWaiter {
    public function waitWithTimeout($pid, $timeout_seconds) {
        echo "PID {$pid} を {$timeout_seconds}秒のタイムアウトで待機\n";
        
        $start_time = time();
        $status = 0;
        
        while (true) {
            // ノンブロッキングで待機
            $result = pcntl_waitpid($pid, $status, WNOHANG);
            
            if ($result === $pid) {
                // プロセスが終了した
                $elapsed = time() - $start_time;
                echo "✓ プロセス終了 (経過時間: {$elapsed}秒)\n";
                
                return [
                    'success' => true,
                    'elapsed' => $elapsed,
                    'status' => $status
                ];
                
            } elseif ($result === -1) {
                // エラー
                echo "✗ エラーが発生しました\n";
                return ['success' => false, 'error' => 'wait_failed'];
                
            } elseif ($result === 0) {
                // まだ実行中
                $elapsed = time() - $start_time;
                
                if ($elapsed >= $timeout_seconds) {
                    // タイムアウト
                    echo "✗ タイムアウト ({$elapsed}秒)\n";
                    echo "プロセスを強制終了します\n";
                    
                    // SIGTERM送信
                    posix_kill($pid, SIGTERM);
                    sleep(1);
                    
                    // まだ生きていればSIGKILL
                    $result = pcntl_waitpid($pid, $status, WNOHANG);
                    if ($result === 0) {
                        posix_kill($pid, SIGKILL);
                        pcntl_waitpid($pid, $status);
                    }
                    
                    return [
                        'success' => false,
                        'error' => 'timeout',
                        'elapsed' => $elapsed
                    ];
                }
            }
            
            usleep(100000); // 0.1秒待機
        }
    }
}

// テスト
$pid = pcntl_fork();

if ($pid > 0) {
    // 親プロセス
    $waiter = new TimedWaiter();
    $result = $waiter->waitWithTimeout($pid, 3);
    
    print_r($result);
    
} elseif ($pid === 0) {
    // 子プロセス(意図的に長時間実行)
    sleep(10);
    exit(0);
}
?>

エラーハンドリングとリトライ

<?php
class RobustWaitpid {
    private $max_retries = 3;
    
    public function waitForProcess($pid) {
        echo "プロセス(PID:{$pid})を待機します\n";
        
        for ($attempt = 1; $attempt <= $this->max_retries; $attempt++) {
            $status = 0;
            $result = pcntl_waitpid($pid, $status);
            
            if ($result === $pid) {
                // 成功
                echo "✓ プロセスが正常に終了しました\n";
                return $this->analyzeStatus($status);
                
            } elseif ($result === -1) {
                // エラー
                $errno = pcntl_get_last_error();
                $errmsg = pcntl_strerror($errno);
                
                echo "✗ エラー発生 (試行 {$attempt}/{$this->max_retries})\n";
                echo "  errno: {$errno}\n";
                echo "  error: {$errmsg}\n";
                
                // リトライ可能なエラーか判定
                if ($errno === EINTR && $attempt < $this->max_retries) {
                    echo "  → システムコールが中断されました。リトライします\n";
                    sleep(1);
                    continue;
                    
                } elseif ($errno === ECHILD) {
                    echo "  → 子プロセスが存在しません\n";
                    return null;
                    
                } else {
                    echo "  → リトライできないエラーです\n";
                    return null;
                }
            }
        }
        
        echo "✗ 最大リトライ回数に達しました\n";
        return null;
    }
    
    private function analyzeStatus($status) {
        $result = [
            'status_code' => $status
        ];
        
        if (pcntl_wifexited($status)) {
            $result['type'] = 'exited';
            $result['exit_code'] = pcntl_wexitstatus($status);
            echo "  終了タイプ: 正常終了\n";
            echo "  終了コード: {$result['exit_code']}\n";
            
        } elseif (pcntl_wifsignaled($status)) {
            $result['type'] = 'signaled';
            $result['signal'] = pcntl_wtermsig($status);
            echo "  終了タイプ: シグナル\n";
            echo "  シグナル番号: {$result['signal']}\n";
            
        } elseif (pcntl_wifstopped($status)) {
            $result['type'] = 'stopped';
            $result['stop_signal'] = pcntl_wstopsig($status);
            echo "  終了タイプ: 停止\n";
            echo "  停止シグナル: {$result['stop_signal']}\n";
        }
        
        return $result;
    }
}

// テスト
$pid = pcntl_fork();

if ($pid > 0) {
    // 親プロセス
    $waiter = new RobustWaitpid();
    $result = $waiter->waitForProcess($pid);
    
    if ($result) {
        echo "\n処理結果:\n";
        print_r($result);
    }
    
} elseif ($pid === 0) {
    // 子プロセス
    sleep(2);
    exit(5);
}
?>

子プロセス群の動的管理

<?php
class DynamicProcessPool {
    private $processes = [];
    private $max_concurrent = 3;
    
    public function addTask($task_id, $duration) {
        // 空きができるまで待つ
        while (count($this->processes) >= $this->max_concurrent) {
            $this->checkCompletions();
            usleep(100000);
        }
        
        $pid = pcntl_fork();
        
        if ($pid > 0) {
            $this->processes[$pid] = [
                'task_id' => $task_id,
                'start_time' => time(),
                'duration' => $duration
            ];
            
            echo "タスク{$task_id}起動: PID={$pid} ";
            echo "(アクティブ: " . count($this->processes) . "/{$this->max_concurrent})\n";
            
        } elseif ($pid === 0) {
            // 子プロセス
            sleep($duration);
            exit(0);
        }
    }
    
    private function checkCompletions() {
        // 全ての子プロセスをノンブロッキングでチェック
        foreach (array_keys($this->processes) as $pid) {
            $status = 0;
            $result = pcntl_waitpid($pid, $status, WNOHANG);
            
            if ($result === $pid) {
                $task = $this->processes[$pid];
                $elapsed = time() - $task['start_time'];
                
                echo "✓ タスク{$task['task_id']}完了: PID={$pid}, 実行時間={$elapsed}秒\n";
                unset($this->processes[$pid]);
            }
        }
    }
    
    public function waitAll() {
        echo "\n残りのタスクを待機中...\n";
        
        while (!empty($this->processes)) {
            $this->checkCompletions();
            usleep(100000);
        }
        
        echo "全タスク完了\n";
    }
}

// 使用例
$pool = new DynamicProcessPool();

echo "タスクを追加していきます\n\n";

// タスクを次々に追加
for ($i = 1; $i <= 10; $i++) {
    $duration = rand(1, 4);
    $pool->addTask($i, $duration);
    usleep(500000); // 0.5秒待機
}

$pool->waitAll();
?>

重要なポイントと注意事項

⚠️ 指定したPIDが存在しない場合

存在しないPIDを指定するとエラーになります:

$result = pcntl_waitpid(99999, $status);

if ($result === -1) {
    $errno = pcntl_get_last_error();
    // ECHILD (子プロセスが存在しない)
    echo pcntl_strerror($errno);
}

💡 WNOHANGの戻り値

WNOHANGフラグ使用時の戻り値に注意:

$result = pcntl_waitpid($pid, $status, WNOHANG);

if ($result === $pid) {
    echo "プロセスが終了した\n";
} elseif ($result === 0) {
    echo "まだ実行中\n";
} elseif ($result === -1) {
    echo "エラー\n";
}

🔍 複数回waitpidを呼ぶとエラー

既に回収したプロセスに対して再度waitpidを呼ぶとエラー:

pcntl_waitpid($pid, $status); // 1回目:成功

pcntl_waitpid($pid, $status); // 2回目:エラー(ECHILD)

ベストプラクティス

1. PIDを記録して管理

$children = [];

$pid = pcntl_fork();
if ($pid > 0) {
    $children[$pid] = ['task' => 'job1', 'start' => time()];
}

// 後で特定のPIDを待つ
pcntl_waitpid($pid, $status);
unset($children[$pid]);

2. WNOHANGで効率的にポーリング

// ブロッキングせずに定期的にチェック
foreach ($child_pids as $pid) {
    $result = pcntl_waitpid($pid, $status, WNOHANG);
    if ($result === $pid) {
        // 終了処理
    }
}

3. エラーチェックは必須

$result = pcntl_waitpid($pid, $status);

if ($result === -1) {
    $errno = pcntl_get_last_error();
    error_log("waitpid failed: " . pcntl_strerror($errno));
}

まとめ

pcntl_waitpid()は、特定の子プロセスを指定して待機できる柔軟な関数です。重要なポイント:

  • ✅ 特定のPIDを指定して待機できる
  • ✅ process_id=-1で任意の子プロセスを待てる
  • ✅ WNOHANGでノンブロッキング動作が可能
  • ✅ 複数の子プロセスを個別に管理できる
  • ✅ タイムアウトや優先度制御が実装できる

複雑なマルチプロセス処理を実装する場合、pcntl_wait()よりpcntl_waitpid()の方が柔軟な制御が可能です。特に、複数の子プロセスを並行実行させながら個別に管理したい場合に最適です!


関連記事

  • pcntl_wait()で任意の子プロセスを待機
  • pcntl_fork()で子プロセスを作成
  • PHPでワーカープールを実装する方法
タイトルとURLをコピーしました