[PHP]pcntl_signal_dispatch関数とは?シグナル処理を実行する方法を徹底解説

PHP

こんにちは!今回はPHPのシグナル処理で絶対に理解しておくべきpcntl_signal_dispatch()関数について、詳しく解説していきます。「シグナルハンドラを設定したのに動かない!」という悩みを抱えている方は必見です!

pcntl_signal_dispatch関数とは?

pcntl_signal_dispatch()は、保留中のシグナルに対するハンドラを呼び出す関数です。pcntl_signal()でシグナルハンドラを設定しただけでは、実際にはハンドラは実行されません。この関数を呼び出すことで、初めてシグナルが処理されます。

基本構文

pcntl_signal_dispatch(): bool
  • 引数: なし
  • 戻り値: 成功時true、失敗時false

なぜpcntl_signal_dispatch()が必要なのか?

PHPのシグナル処理はデフォルトでは非同期ではありません。つまり:

  1. シグナルを受信する(例:Ctrl+C)
  2. シグナルは一旦保留される
  3. pcntl_signal_dispatch()が呼ばれた時に初めてハンドラが実行される

この仕組みにより、PHPスクリプトの実行中に予期しないタイミングでハンドラが実行されるのを防いでいます。

視覚的な理解

[シグナル受信] → [保留キュー] → [pcntl_signal_dispatch()] → [ハンドラ実行]
     (SIGINT)         待機中              呼び出し               処理実行

実践的なコード例

基本的な使い方

<?php
$running = true;

// シグナルハンドラを設定
pcntl_signal(SIGINT, function($signal) use (&$running) {
    echo "\nSIGINTを受信しました。終了します...\n";
    $running = false;
});

echo "プログラム実行中 (Ctrl+Cで終了)\n";

while ($running) {
    echo "処理中...\n";
    sleep(1);
    
    // ★重要:シグナルディスパッチを呼ぶ!
    pcntl_signal_dispatch();
}

echo "プログラムが正常に終了しました\n";
?>

ディスパッチを忘れた場合(動かない例)

<?php
pcntl_signal(SIGINT, function($signal) {
    echo "\nこれは表示されません!\n";
    exit(0);
});

echo "Ctrl+Cを押しても反応しません...\n";

while (true) {
    echo "処理中...\n";
    sleep(1);
    
    // pcntl_signal_dispatch()を呼んでいない!
    // ハンドラは実行されない
}
?>

適切なディスパッチの配置

<?php
class TaskProcessor {
    private $running = true;
    private $tasks = [];
    
    public function __construct() {
        pcntl_signal(SIGTERM, [$this, 'handleShutdown']);
        pcntl_signal(SIGINT, [$this, 'handleShutdown']);
    }
    
    public function handleShutdown($signal) {
        echo "\n終了シグナルを受信。現在のタスクを完了してから終了します...\n";
        $this->running = false;
    }
    
    public function run() {
        echo "タスク処理開始: PID=" . getmypid() . "\n";
        
        while ($this->running) {
            // 各タスクの前にシグナルをチェック
            pcntl_signal_dispatch();
            
            if (!$this->running) {
                break;
            }
            
            $this->processTask();
        }
        
        echo "全てのタスクが完了しました\n";
    }
    
    private function processTask() {
        echo "タスク実行中...\n";
        
        // 長時間の処理の途中でもシグナルチェック
        for ($i = 0; $i < 5; $i++) {
            sleep(1);
            pcntl_signal_dispatch(); // 定期的に呼ぶ
            
            if (!$this->running) {
                echo "処理を中断します\n";
                return;
            }
        }
        
        echo "タスク完了\n";
    }
}

$processor = new TaskProcessor();
$processor->run();
?>

ディスパッチの頻度を調整する

<?php
$running = true;
$dispatch_counter = 0;

pcntl_signal(SIGTERM, function() use (&$running) {
    echo "\n終了します\n";
    $running = false;
});

while ($running) {
    // 軽い処理を実行
    echo ".";
    usleep(100000); // 0.1秒
    
    $dispatch_counter++;
    
    // 10回に1回だけディスパッチ(パフォーマンス最適化)
    if ($dispatch_counter % 10 === 0) {
        pcntl_signal_dispatch();
    }
}

echo "\n完了\n";
?>

複数のシグナルを処理

<?php
$running = true;
$reload_config = false;

// 複数のシグナルハンドラを設定
pcntl_signal(SIGTERM, function() use (&$running) {
    echo "\nSIGTERM: 終了します\n";
    $running = false;
});

pcntl_signal(SIGINT, function() use (&$running) {
    echo "\nSIGINT: 終了します\n";
    $running = false;
});

pcntl_signal(SIGHUP, function() use (&$reload_config) {
    echo "\nSIGHUP: 設定を再読み込みします\n";
    $reload_config = true;
});

pcntl_signal(SIGUSR1, function() {
    echo "\nSIGUSR1: ステータスを表示\n";
    echo "PID: " . getmypid() . ", メモリ: " . memory_get_usage() . " bytes\n";
});

echo "プロセス起動: PID=" . getmypid() . "\n";
echo "kill -HUP " . getmypid() . " で設定リロード\n";
echo "kill -USR1 " . getmypid() . " でステータス表示\n";

while ($running) {
    // 設定リロードフラグをチェック
    if ($reload_config) {
        echo "設定ファイルを再読み込み中...\n";
        // 実際の設定読み込み処理
        $reload_config = false;
    }
    
    echo "処理中...\n";
    sleep(2);
    
    // 全てのシグナルをまとめて処理
    pcntl_signal_dispatch();
}

echo "プログラム終了\n";
?>

エラーハンドリング付き

<?php
function safe_signal_dispatch() {
    $result = pcntl_signal_dispatch();
    
    if ($result === false) {
        $errno = pcntl_get_last_error();
        $error_msg = pcntl_strerror($errno);
        error_log("シグナルディスパッチエラー: {$error_msg}");
        return false;
    }
    
    return true;
}

$running = true;

pcntl_signal(SIGINT, function() use (&$running) {
    echo "\n終了します\n";
    $running = false;
});

while ($running) {
    echo "処理中...\n";
    sleep(1);
    
    if (!safe_signal_dispatch()) {
        echo "シグナル処理でエラーが発生しました\n";
        break;
    }
}
?>

pcntl_async_signals()との関係

PHP 7.1以降では、pcntl_async_signals()を使うことで、pcntl_signal_dispatch()を呼ばなくても自動的にシグナルが処理されます。

従来の方法(PHP 7.0以前)

<?php
pcntl_signal(SIGINT, function() {
    echo "終了\n";
    exit(0);
});

while (true) {
    echo "処理中\n";
    sleep(1);
    pcntl_signal_dispatch(); // 必須!
}
?>

新しい方法(PHP 7.1以降)

<?php
// 非同期シグナル処理を有効化
pcntl_async_signals(true);

pcntl_signal(SIGINT, function() {
    echo "終了\n";
    exit(0);
});

while (true) {
    echo "処理中\n";
    sleep(1);
    // pcntl_signal_dispatch()は不要!
}
?>

どちらを使うべき?

方法メリットデメリット
pcntl_signal_dispatch()タイミングを完全制御、予測可能な動作呼び忘れのリスク、コードが冗長
pcntl_async_signals(true)コードがシンプル、呼び忘れがない予期しないタイミングでハンドラ実行の可能性

推奨: PHP 7.1以降ならpcntl_async_signals(true)を使うのが一般的ですが、厳密な制御が必要な場合はpcntl_signal_dispatch()を使いましょう。

パフォーマンスの考慮

ディスパッチの呼び出し頻度

<?php
// ❌ 悪い例:過度に頻繁な呼び出し
while (true) {
    // 軽い処理
    $x = 1 + 1;
    pcntl_signal_dispatch(); // オーバーヘッドが大きい
}

// ✅ 良い例:適切な間隔
$counter = 0;
while (true) {
    // 軽い処理
    $x = 1 + 1;
    
    if (++$counter % 1000 === 0) {
        pcntl_signal_dispatch(); // 1000回に1回
    }
}

// ✅ より良い例:時間ベース
$last_dispatch = microtime(true);
while (true) {
    // 処理
    
    $now = microtime(true);
    if ($now - $last_dispatch >= 0.1) { // 100msごと
        pcntl_signal_dispatch();
        $last_dispatch = $now;
    }
}
?>

デバッグとトラブルシューティング

シグナルが処理されない場合のチェックリスト

<?php
// 1. PCNTL拡張が有効か確認
if (!function_exists('pcntl_signal_dispatch')) {
    die("PCNTL拡張が利用できません\n");
}

// 2. CLIモードか確認
if (php_sapi_name() !== 'cli') {
    die("CLIモードでのみ動作します\n");
}

// 3. シグナルハンドラが正しく設定されているか
$result = pcntl_signal(SIGINT, function() {
    echo "ハンドラ実行\n";
});

if (!$result) {
    die("シグナルハンドラの設定に失敗\n");
}

// 4. ディスパッチを呼んでいるか確認
$dispatch_count = 0;
while (true) {
    echo "ループ {$dispatch_count}\n";
    sleep(1);
    
    pcntl_signal_dispatch();
    $dispatch_count++;
    
    if ($dispatch_count > 10) break;
}
?>

ディスパッチのデバッグ

<?php
function debug_signal_dispatch() {
    static $call_count = 0;
    $call_count++;
    
    echo "[DEBUG] pcntl_signal_dispatch() 呼び出し #{$call_count}\n";
    
    $result = pcntl_signal_dispatch();
    
    if ($result === false) {
        echo "[ERROR] ディスパッチ失敗\n";
    }
    
    return $result;
}

pcntl_signal(SIGINT, function($signal) {
    echo "[SIGNAL] SIGINTを受信しました (signal={$signal})\n";
    exit(0);
});

while (true) {
    echo "処理中...\n";
    sleep(1);
    debug_signal_dispatch();
}
?>

ベストプラクティス

1. 適切な配置

// ✅ ループの先頭で呼ぶ
while ($running) {
    pcntl_signal_dispatch();
    // 処理
}

// ✅ 長時間処理の間に呼ぶ
function long_task() {
    for ($i = 0; $i < 1000; $i++) {
        // 処理
        if ($i % 100 === 0) {
            pcntl_signal_dispatch();
        }
    }
}

2. 例外処理との組み合わせ

<?php
$running = true;

pcntl_signal(SIGTERM, function() use (&$running) {
    $running = false;
});

while ($running) {
    try {
        pcntl_signal_dispatch();
        
        // 処理
        risky_operation();
        
    } catch (Exception $e) {
        error_log("エラー: " . $e->getMessage());
    }
}
?>

3. クリーンアップ処理

<?php
$running = true;

pcntl_signal(SIGTERM, function() use (&$running) {
    echo "終了処理開始\n";
    $running = false;
});

try {
    while ($running) {
        pcntl_signal_dispatch();
        // 処理
        sleep(1);
    }
} finally {
    // 必ず実行されるクリーンアップ
    echo "クリーンアップ実行\n";
}
?>

よくある間違い

❌ 間違い1: ブロッキング処理後に呼ばない

while (true) {
    sleep(10); // 長時間ブロック
    pcntl_signal_dispatch(); // 10秒間シグナルが処理されない!
}

✅ 修正版

while (true) {
    for ($i = 0; $i < 10; $i++) {
        sleep(1);
        pcntl_signal_dispatch(); // 1秒ごとにチェック
    }
}

❌ 間違い2: ハンドラ内で呼ぶ

pcntl_signal(SIGINT, function() {
    pcntl_signal_dispatch(); // 不要!再帰的になる可能性
    exit(0);
});

まとめ

pcntl_signal_dispatch()は、PHPでシグナル処理を実装する際の要となる関数です。重要なポイント:

  • ✅ シグナルハンドラは設定しただけでは動かない
  • pcntl_signal_dispatch()を定期的に呼ぶ必要がある
  • ✅ PHP 7.1以降はpcntl_async_signals(true)で自動化可能
  • ✅ ループや長時間処理の中で適切に配置する
  • ✅ パフォーマンスを考慮して呼び出し頻度を調整

長時間動作するCLIアプリケーションやデーモンプロセスを開発する際は、この関数の適切な使用が、堅牢なシグナル処理の鍵となります!


関連記事

  • pcntl_signal()でシグナルハンドラを設定する
  • pcntl_async_signals()で非同期処理を実装
  • PHPでグレースフルシャットダウンを実装する方法

タイトルとURLをコピーしました