[PHP]7.1の新機能!pcntl_async_signals関数で非同期シグナル処理を簡単に

PHP

はじめに

PHPでプロセス制御を行う際、シグナル処理は重要な要素です。しかし、PHP 7.1より前は、シグナルを受け取るために pcntl_signal_dispatch() を明示的に呼び出す必要がありました。

PHP 7.1で導入された pcntl_async_signals 関数は、この面倒な作業を不要にし、シグナルを自動的に非同期で処理できるようにします。この記事では、モダンなPHPにおけるシグナル処理の新しい方法について詳しく解説します。

pcntl_async_signals関数とは

pcntl_async_signals は、シグナルを非同期で処理するかどうかを設定する関数です。有効にすると、pcntl_signal_dispatch() を明示的に呼び出さなくても、シグナルハンドラが自動的に実行されます。

基本構文

bool pcntl_async_signals([bool $enable = null])

パラメータ

  • $enable (bool, optional):
    • true: 非同期シグナル処理を有効化
    • false: 非同期シグナル処理を無効化
    • null (省略時): 現在の設定を取得

戻り値

  • bool: パラメータを指定した場合は常にtrue、省略した場合は現在の設定状態

対応バージョン

  • PHP 7.1.0以降で利用可能

前提条件

<?php
// 環境チェック
if (php_sapi_name() !== 'cli') {
    die("この機能はCLI環境でのみ利用可能です。\n");
}

if (!function_exists('pcntl_async_signals')) {
    die("pcntl_async_signals関数が利用できません(PHP 7.1.0以降が必要)。\n");
}

if (version_compare(PHP_VERSION, '7.1.0', '<')) {
    die("PHP 7.1.0以降が必要です(現在: " . PHP_VERSION . ")。\n");
}

echo "✓ 環境チェック完了\n";
?>

従来の方法との比較

従来の方法(PHP 7.0以前)

<?php
// 従来の方法: pcntl_signal_dispatch()を明示的に呼び出す必要がある

$received_signal = false;

// シグナルハンドラ登録
pcntl_signal(SIGTERM, function($signo) use (&$received_signal) {
    echo "SIGTERMを受信しました\n";
    $received_signal = true;
});

echo "プロセス起動(PID: " . getmypid() . ")\n";
echo "別ターミナルから 'kill -TERM " . getmypid() . "' を実行してください\n\n";

// メインループ
while (!$received_signal) {
    echo "処理中...\n";
    
    // 重要: シグナルを処理するために明示的な呼び出しが必要
    pcntl_signal_dispatch();
    
    sleep(1);
}

echo "終了します\n";
?>

新しい方法(PHP 7.1以降)

<?php
// 新しい方法: 非同期シグナル処理を有効化

$received_signal = false;

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

// シグナルハンドラ登録
pcntl_signal(SIGTERM, function($signo) use (&$received_signal) {
    echo "SIGTERMを受信しました\n";
    $received_signal = true;
});

echo "プロセス起動(PID: " . getmypid() . ")\n";
echo "別ターミナルから 'kill -TERM " . getmypid() . "' を実行してください\n\n";

// メインループ
while (!$received_signal) {
    echo "処理中...\n";
    
    // pcntl_signal_dispatch()の呼び出しが不要!
    
    sleep(1);
}

echo "終了します\n";
?>

基本的な使用例

シンプルな非同期シグナル処理

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

$signals_received = [];

// 複数のシグナルハンドラを登録
pcntl_signal(SIGTERM, function($signo) use (&$signals_received) {
    $signals_received[] = 'SIGTERM';
    echo "[" . date('H:i:s') . "] SIGTERM受信\n";
});

pcntl_signal(SIGINT, function($signo) use (&$signals_received) {
    $signals_received[] = 'SIGINT';
    echo "[" . date('H:i:s') . "] SIGINT受信(Ctrl+C)\n";
    exit(0);
});

pcntl_signal(SIGUSR1, function($signo) use (&$signals_received) {
    $signals_received[] = 'SIGUSR1';
    echo "[" . date('H:i:s') . "] SIGUSR1受信(カスタムシグナル)\n";
});

echo "PID: " . getmypid() . "\n";
echo "Ctrl+Cで終了、または以下のコマンドでシグナル送信:\n";
echo "  kill -USR1 " . getmypid() . "\n";
echo "  kill -TERM " . getmypid() . "\n\n";

// シンプルなメインループ
$counter = 0;
while (true) {
    echo "実行中... {$counter}\n";
    $counter++;
    sleep(2);
}
?>

設定状態の確認

<?php
// 現在の設定を取得
$current_state = pcntl_async_signals();
echo "現在の非同期シグナル設定: " . ($current_state ? '有効' : '無効') . "\n";

// 有効化
pcntl_async_signals(true);
echo "非同期シグナルを有効化しました\n";
echo "設定: " . (pcntl_async_signals() ? '有効' : '無効') . "\n\n";

// 無効化
pcntl_async_signals(false);
echo "非同期シグナルを無効化しました\n";
echo "設定: " . (pcntl_async_signals() ? '有効' : '無効') . "\n";
?>

実践的な活用例

1. グレースフルシャットダウンの実装

<?php
class GracefulShutdownHandler {
    private $is_shutting_down = false;
    private $tasks_running = 0;
    private $start_time;
    
    public function __construct() {
        $this->start_time = time();
        
        // 非同期シグナル処理を有効化
        pcntl_async_signals(true);
        
        // シャットダウンシグナルを登録
        pcntl_signal(SIGTERM, [$this, 'handleShutdown']);
        pcntl_signal(SIGINT, [$this, 'handleShutdown']);
        
        echo "グレースフルシャットダウンハンドラを初期化しました\n";
        echo "PID: " . getmypid() . "\n\n";
    }
    
    /**
     * シャットダウンシグナルハンドラ
     */
    public function handleShutdown($signo) {
        $signal_name = $signo === SIGTERM ? 'SIGTERM' : 'SIGINT';
        echo "\n[" . date('H:i:s') . "] {$signal_name}を受信しました\n";
        
        if ($this->is_shutting_down) {
            echo "既にシャットダウン処理中です\n";
            return;
        }
        
        $this->is_shutting_down = true;
        echo "グレースフルシャットダウンを開始します...\n";
        
        // 実行中のタスクを待機
        $this->waitForTasks();
        
        // クリーンアップ処理
        $this->cleanup();
        
        $uptime = time() - $this->start_time;
        echo "稼働時間: {$uptime}秒\n";
        echo "正常終了しました\n";
        
        exit(0);
    }
    
    /**
     * タスク実行(シミュレーション)
     */
    public function runTask($task_id, $duration) {
        if ($this->is_shutting_down) {
            echo "シャットダウン中のため、新規タスクは受け付けません\n";
            return false;
        }
        
        $this->tasks_running++;
        echo "[タスク{$task_id}] 開始(予定時間: {$duration}秒)\n";
        
        sleep($duration);
        
        echo "[タスク{$task_id}] 完了\n";
        $this->tasks_running--;
        
        return true;
    }
    
    /**
     * 実行中タスクの待機
     */
    private function waitForTasks() {
        if ($this->tasks_running > 0) {
            echo "実行中のタスク({$this->tasks_running}個)の完了を待機中...\n";
            
            $timeout = 30; // 最大30秒待機
            $elapsed = 0;
            
            while ($this->tasks_running > 0 && $elapsed < $timeout) {
                sleep(1);
                $elapsed++;
                echo "  待機中... 残りタスク: {$this->tasks_running}\n";
            }
            
            if ($this->tasks_running > 0) {
                echo "⚠️  タイムアウト: {$this->tasks_running}個のタスクが未完了\n";
            } else {
                echo "✓ すべてのタスクが完了しました\n";
            }
        }
    }
    
    /**
     * クリーンアップ処理
     */
    private function cleanup() {
        echo "クリーンアップ処理を実行中...\n";
        
        // データベース接続のクローズ
        echo "  - データベース接続をクローズ\n";
        
        // 一時ファイルの削除
        echo "  - 一時ファイルを削除\n";
        
        // ログファイルのフラッシュ
        echo "  - ログファイルをフラッシュ\n";
        
        echo "✓ クリーンアップ完了\n";
    }
    
    /**
     * メインループ
     */
    public function run() {
        $task_counter = 1;
        
        while (!$this->is_shutting_down) {
            echo "\n[" . date('H:i:s') . "] タスクを実行します\n";
            
            $this->runTask($task_counter, rand(1, 3));
            $task_counter++;
            
            sleep(2);
        }
    }
}

// 使用例
$handler = new GracefulShutdownHandler();
$handler->run();
?>

2. ワーカープロセスの管理

<?php
class WorkerManager {
    private $workers = [];
    private $max_workers = 5;
    private $should_stop = false;
    
    public function __construct($max_workers = 5) {
        $this->max_workers = $max_workers;
        
        // 非同期シグナル処理を有効化
        pcntl_async_signals(true);
        
        // シグナルハンドラ登録
        pcntl_signal(SIGTERM, [$this, 'handleTerminate']);
        pcntl_signal(SIGINT, [$this, 'handleTerminate']);
        pcntl_signal(SIGCHLD, [$this, 'handleChildExit']);
        pcntl_signal(SIGUSR1, [$this, 'handleStatus']);
        
        echo "ワーカーマネージャーを初期化しました\n";
        echo "PID: " . getmypid() . "\n";
        echo "最大ワーカー数: {$this->max_workers}\n";
        echo "ステータス確認: kill -USR1 " . getmypid() . "\n\n";
    }
    
    /**
     * 終了シグナルハンドラ
     */
    public function handleTerminate($signo) {
        echo "\n終了シグナルを受信しました\n";
        $this->should_stop = true;
        $this->stopAllWorkers();
    }
    
    /**
     * 子プロセス終了ハンドラ
     */
    public function handleChildExit($signo) {
        // ゾンビプロセスを回収
        while (($pid = pcntl_waitpid(-1, $status, WNOHANG)) > 0) {
            if (isset($this->workers[$pid])) {
                $worker = $this->workers[$pid];
                $exit_code = pcntl_wexitstatus($status);
                
                echo "[ワーカー{$worker['id']}] 終了(PID: {$pid}, 終了コード: {$exit_code})\n";
                unset($this->workers[$pid]);
            }
        }
    }
    
    /**
     * ステータス表示ハンドラ
     */
    public function handleStatus($signo) {
        echo "\n=== ワーカーステータス ===\n";
        echo "稼働中のワーカー数: " . count($this->workers) . " / {$this->max_workers}\n";
        
        foreach ($this->workers as $pid => $worker) {
            $uptime = time() - $worker['start_time'];
            echo "  ワーカー{$worker['id']} (PID: {$pid}) - 稼働時間: {$uptime}秒\n";
        }
        echo "======================\n\n";
    }
    
    /**
     * ワーカーを起動
     */
    public function spawnWorker($worker_id) {
        $pid = pcntl_fork();
        
        if ($pid === -1) {
            echo "ワーカーの起動に失敗しました\n";
            return false;
        } elseif ($pid === 0) {
            // 子プロセス
            $this->workerProcess($worker_id);
            exit(0);
        } else {
            // 親プロセス
            $this->workers[$pid] = [
                'id' => $worker_id,
                'pid' => $pid,
                'start_time' => time()
            ];
            echo "[ワーカー{$worker_id}] 起動(PID: {$pid})\n";
            return true;
        }
    }
    
    /**
     * ワーカープロセスの処理
     */
    private function workerProcess($worker_id) {
        // 子プロセスでも非同期シグナルを有効化
        pcntl_async_signals(true);
        
        $stop = false;
        pcntl_signal(SIGTERM, function() use (&$stop) {
            $stop = true;
        });
        
        echo "[ワーカー{$worker_id}] 処理開始\n";
        
        $job_count = 0;
        while (!$stop && $job_count < 10) {
            echo "[ワーカー{$worker_id}] ジョブ{$job_count}を処理中\n";
            sleep(rand(1, 3));
            $job_count++;
        }
        
        echo "[ワーカー{$worker_id}] 処理完了({$job_count}ジョブ)\n";
    }
    
    /**
     * すべてのワーカーを停止
     */
    private function stopAllWorkers() {
        echo "すべてのワーカーに停止シグナルを送信します\n";
        
        foreach ($this->workers as $pid => $worker) {
            echo "  ワーカー{$worker['id']} (PID: {$pid}) に停止シグナル送信\n";
            posix_kill($pid, SIGTERM);
        }
        
        // すべてのワーカーが終了するまで待機
        $timeout = 10;
        $elapsed = 0;
        
        while (count($this->workers) > 0 && $elapsed < $timeout) {
            sleep(1);
            $elapsed++;
        }
        
        if (count($this->workers) > 0) {
            echo "⚠️  一部のワーカーが応答しません。強制終了します\n";
            foreach ($this->workers as $pid => $worker) {
                posix_kill($pid, SIGKILL);
            }
        } else {
            echo "✓ すべてのワーカーが正常に終了しました\n";
        }
    }
    
    /**
     * マネージャーを実行
     */
    public function run() {
        // 初期ワーカーを起動
        for ($i = 1; $i <= $this->max_workers; $i++) {
            $this->spawnWorker($i);
            usleep(100000); // 0.1秒待機
        }
        
        // メインループ
        while (!$this->should_stop) {
            sleep(1);
        }
        
        echo "ワーカーマネージャーを終了します\n";
    }
}

// 使用例
$manager = new WorkerManager(3);
$manager->run();
?>

3. 動的な設定リロード

<?php
class ConfigReloadHandler {
    private $config = [];
    private $config_file;
    private $last_reload;
    
    public function __construct($config_file) {
        $this->config_file = $config_file;
        
        // 非同期シグナル処理を有効化
        pcntl_async_signals(true);
        
        // SIGHUP(設定リロード用の慣習的なシグナル)を登録
        pcntl_signal(SIGHUP, [$this, 'handleReload']);
        
        // 初期設定を読み込み
        $this->loadConfig();
        
        echo "設定リロードハンドラーを初期化しました\n";
        echo "PID: " . getmypid() . "\n";
        echo "設定リロード: kill -HUP " . getmypid() . "\n\n";
    }
    
    /**
     * 設定リロードハンドラ
     */
    public function handleReload($signo) {
        echo "\n[" . date('H:i:s') . "] SIGHUPを受信 - 設定をリロードします\n";
        
        $old_config = $this->config;
        $this->loadConfig();
        
        // 変更された設定を表示
        $this->displayConfigChanges($old_config, $this->config);
    }
    
    /**
     * 設定ファイルを読み込み
     */
    private function loadConfig() {
        if (!file_exists($this->config_file)) {
            echo "⚠️  設定ファイルが見つかりません: {$this->config_file}\n";
            $this->createDefaultConfig();
        }
        
        $json = file_get_contents($this->config_file);
        $this->config = json_decode($json, true) ?? [];
        $this->last_reload = time();
        
        echo "✓ 設定を読み込みました: " . count($this->config) . "項目\n";
    }
    
    /**
     * デフォルト設定を作成
     */
    private function createDefaultConfig() {
        $default_config = [
            'app_name' => 'MyApp',
            'debug' => false,
            'max_connections' => 100,
            'timeout' => 30
        ];
        
        file_put_contents(
            $this->config_file,
            json_encode($default_config, JSON_PRETTY_PRINT)
        );
        
        echo "デフォルト設定ファイルを作成しました\n";
    }
    
    /**
     * 設定変更を表示
     */
    private function displayConfigChanges($old_config, $new_config) {
        echo "\n=== 設定変更 ===\n";
        
        $all_keys = array_unique(array_merge(
            array_keys($old_config),
            array_keys($new_config)
        ));
        
        $changes_found = false;
        
        foreach ($all_keys as $key) {
            $old_value = $old_config[$key] ?? null;
            $new_value = $new_config[$key] ?? null;
            
            if ($old_value !== $new_value) {
                $changes_found = true;
                
                if ($old_value === null) {
                    echo "[追加] {$key}: " . json_encode($new_value) . "\n";
                } elseif ($new_value === null) {
                    echo "[削除] {$key}\n";
                } else {
                    echo "[変更] {$key}: " . json_encode($old_value) . 
                         " → " . json_encode($new_value) . "\n";
                }
            }
        }
        
        if (!$changes_found) {
            echo "変更はありませんでした\n";
        }
        
        echo "===============\n\n";
    }
    
    /**
     * 現在の設定を表示
     */
    public function displayCurrentConfig() {
        echo "\n=== 現在の設定 ===\n";
        foreach ($this->config as $key => $value) {
            echo "{$key}: " . json_encode($value) . "\n";
        }
        echo "最終リロード: " . date('Y-m-d H:i:s', $this->last_reload) . "\n";
        echo "================\n\n";
    }
    
    /**
     * 設定値を取得
     */
    public function get($key, $default = null) {
        return $this->config[$key] ?? $default;
    }
    
    /**
     * メインループ
     */
    public function run() {
        $counter = 0;
        
        while (true) {
            if ($counter % 10 === 0) {
                $this->displayCurrentConfig();
            }
            
            echo "実行中... {$counter} (debug=" . 
                 ($this->get('debug') ? 'true' : 'false') . ")\n";
            
            $counter++;
            sleep(2);
        }
    }
}

// 使用例
$config_file = '/tmp/app_config.json';
$handler = new ConfigReloadHandler($config_file);

echo "設定ファイルを編集してから以下を実行:\n";
echo "  kill -HUP " . getmypid() . "\n\n";

$handler->run();
?>

4. パフォーマンス統計の収集

<?php
class PerformanceMonitor {
    private $stats = [
        'requests_processed' => 0,
        'total_time' => 0,
        'min_time' => PHP_FLOAT_MAX,
        'max_time' => 0,
        'errors' => 0
    ];
    private $start_time;
    
    public function __construct() {
        $this->start_time = microtime(true);
        
        // 非同期シグナル処理を有効化
        pcntl_async_signals(true);
        
        // 統計表示用のシグナルを登録
        pcntl_signal(SIGUSR1, [$this, 'displayStats']);
        pcntl_signal(SIGUSR2, [$this, 'resetStats']);
        
        echo "パフォーマンスモニターを初期化しました\n";
        echo "PID: " . getmypid() . "\n";
        echo "統計表示: kill -USR1 " . getmypid() . "\n";
        echo "統計リセット: kill -USR2 " . getmypid() . "\n\n";
    }
    
    /**
     * 統計表示ハンドラ
     */
    public function displayStats($signo) {
        echo "\n" . str_repeat("=", 60) . "\n";
        echo "パフォーマンス統計\n";
        echo str_repeat("=", 60) . "\n";
        
        $uptime = microtime(true) - $this->start_time;
        $avg_time = $this->stats['requests_processed'] > 0
            ? $this->stats['total_time'] / $this->stats['requests_processed']
            : 0;
        
        $rps = $this->stats['requests_processed'] / $uptime;
        
        echo sprintf("稼働時間: %.2f秒\n", $uptime);
        echo sprintf("処理リクエスト数: %d\n", $this->stats['requests_processed']);
        echo sprintf("リクエスト/秒: %.2f\n", $rps);
        echo sprintf("平均処理時間: %.4f秒\n", $avg_time);
        echo sprintf("最小処理時間: %.4f秒\n", 
            $this->stats['min_time'] === PHP_FLOAT_MAX ? 0 : $this->stats['min_time']);
        echo sprintf("最大処理時間: %.4f秒\n", $this->stats['max_time']);
        echo sprintf("エラー数: %d\n", $this->stats['errors']);
        
        if ($this->stats['requests_processed'] > 0) {
            $error_rate = ($this->stats['errors'] / $this->stats['requests_processed']) * 100;
            echo sprintf("エラー率: %.2f%%\n", $error_rate);
        }
        
        echo str_repeat("=", 60) . "\n\n";
    }
    
    /**
     * 統計リセットハンドラ
     */
    public function resetStats($signo) {
        echo "\n統計をリセットします\n";
        
        $this->stats = [
            'requests_processed' => 0,
            'total_time' => 0,
            'min_time' => PHP_FLOAT_MAX,
            'max_time' => 0,
            'errors' => 0
        ];
        $this->start_time = microtime(true);
        
        echo "✓ 統計をリセットしました\n\n";
    }
    
    /**
     * リクエスト処理(シミュレーション)
     */
    public function processRequest() {
        $start = microtime(true);
        
        // ランダムな処理時間
        $processing_time = rand(10, 500) / 1000; // 0.01〜0.5秒
        usleep($processing_time * 1000000);
        
        // ランダムにエラーを発生させる(5%の確率)
        $has_error = rand(1, 100) <= 5;
        
        $elapsed = microtime(true) - $start;
        
        // 統計を更新
        $this->stats['requests_processed']++;
        $this->stats['total_time'] += $elapsed;
        $this->stats['min_time'] = min($this->stats['min_time'], $elapsed);
        $this->stats['max_time'] = max($this->stats['max_time'], $elapsed);
        
        if ($has_error) {
            $this->stats['errors']++;
            echo "✗ ";
        } else {
            echo "✓ ";
        }
        
        if ($this->stats['requests_processed'] % 50 === 0) {
            echo "\n";
        }
    }
    
    /**
     * メインループ
     */
    public function run() {
        echo "リクエスト処理を開始します...\n\n";
        
        while (true) {
            $this->processRequest();
            usleep(100000); // 0.1秒
        }
    }
}

// 使用例
$monitor = new PerformanceMonitor();
$monitor->run();
?>

注意点とベストプラクティス

リエントラント安全性への配慮

<?php
/**
 * シグナルハンドラはいつでも実行される可能性があるため、
 * リエントラント安全性を考慮する必要がある
 */

pcntl_async_signals(true);

$processing = false;
$signal_count = 0;

pcntl_signal(SIGUSR1, function($signo) use (&$processing, &$signal_count) {
    $signal_count++;
    
    // 処理中にシグナルが来た場合の対応
    if ($processing) {
        echo "[⚠️  警告] 処理中にシグナルを受信しました({$signal_count}回目)\n";
        // 複雑な処理は避け、フラグのみ設定
        return;
    }
    
    echo "[✓] シグナル処理: {$signal_count}回目\n";
});

echo "PID: " . getmypid() . "\n";
echo "シグナル送信: kill -USR1 " . getmypid() . "\n\n";

$counter = 0;
while (true) {
    $processing = true;
    echo "処理中... {$counter}\n";
    sleep(2);
    $processing = false;
    
    $counter++;
}
?>

パフォーマンスへの影響

<?php
/**
 * 非同期シグナル処理のパフォーマンステスト
 */

function benchmarkSignalHandling($async_enabled, $iterations = 100000) {
    pcntl_async_signals($async_enabled);
    
    $signal_received = false;
    pcntl_signal(SIGUSR1, function($signo) use (&$signal_received) {
        $signal_received = true;
    });
    
    $start = microtime(true);
    
    for ($i = 0; $i < $iterations; $i++) {
        if (!$async_enabled) {
            pcntl_signal_dispatch(); // 従来の方法
        }
        // 何か処理
        $dummy = $i * 2;
    }
    
    $elapsed = microtime(true) - $start;
    
    return [
        'mode' => $async_enabled ? '非同期' : '同期',
        'iterations' => $iterations,
        'time' => $elapsed,
        'per_iteration' => $elapsed / $iterations * 1000000 // マイクロ秒
    ];
}

echo "=== シグナル処理のパフォーマンス比較 ===\n\n";

// 同期モードでベンチマーク
$sync_result = benchmarkSignalHandling(false, 100000);
echo "同期モード(pcntl_signal_dispatch使用):\n";
echo sprintf("  合計時間: %.4f秒\n", $sync_result['time']);
echo sprintf("  1回あたり: %.4fμs\n\n", $sync_result['per_iteration']);

// 非同期モードでベンチマーク
$async_result = benchmarkSignalHandling(true, 100000);
echo "非同期モード(pcntl_async_signals有効):\n";
echo sprintf("  合計時間: %.4f秒\n", $async_result['time']);
echo sprintf("  1回あたり: %.4fμs\n\n", $async_result['per_iteration']);

// 比較
$diff = (($sync_result['time'] - $async_result['time']) / $sync_result['time']) * 100;
echo "パフォーマンス差: " . number_format(abs($diff), 2) . "%\n";
echo ($diff > 0 ? "非同期モードの方が高速" : "同期モードの方が高速") . "\n";
?>

エラーハンドリング

<?php
class SafeSignalHandler {
    private $handlers = [];
    
    public function __construct() {
        // 非同期シグナル処理を有効化
        if (!pcntl_async_signals(true)) {
            throw new RuntimeException("非同期シグナル処理を有効化できませんでした");
        }
    }
    
    /**
     * 安全なシグナルハンドラ登録
     */
    public function register($signal, $callback, $error_handler = null) {
        if (!is_callable($callback)) {
            throw new InvalidArgumentException("コールバックが無効です");
        }
        
        $safe_callback = function($signo) use ($callback, $error_handler) {
            try {
                call_user_func($callback, $signo);
            } catch (Exception $e) {
                // エラーハンドラが指定されていれば実行
                if (is_callable($error_handler)) {
                    call_user_func($error_handler, $e);
                } else {
                    // デフォルトのエラー処理
                    error_log("シグナルハンドラでエラー: " . $e->getMessage());
                }
            }
        };
        
        if (!pcntl_signal($signal, $safe_callback)) {
            throw new RuntimeException("シグナルハンドラの登録に失敗しました");
        }
        
        $this->handlers[$signal] = [
            'callback' => $callback,
            'error_handler' => $error_handler,
            'registered_at' => time()
        ];
        
        return true;
    }
    
    /**
     * シグナルハンドラを解除
     */
    public function unregister($signal) {
        if (pcntl_signal($signal, SIG_DFL)) {
            unset($this->handlers[$signal]);
            return true;
        }
        return false;
    }
    
    /**
     * 登録済みハンドラの一覧
     */
    public function listHandlers() {
        echo "=== 登録済みシグナルハンドラ ===\n";
        foreach ($this->handlers as $signal => $info) {
            $signal_name = $this->getSignalName($signal);
            $uptime = time() - $info['registered_at'];
            echo "  {$signal_name} (登録後 {$uptime}秒)\n";
        }
        echo "==============================\n";
    }
    
    /**
     * シグナル名を取得
     */
    private function getSignalName($signal) {
        $signals = [
            SIGTERM => 'SIGTERM',
            SIGINT => 'SIGINT',
            SIGHUP => 'SIGHUP',
            SIGUSR1 => 'SIGUSR1',
            SIGUSR2 => 'SIGUSR2',
            SIGCHLD => 'SIGCHLD',
            SIGALRM => 'SIGALRM'
        ];
        
        return $signals[$signal] ?? "SIGNAL_{$signal}";
    }
}

// 使用例
try {
    $handler = new SafeSignalHandler();
    
    // 正常なハンドラ
    $handler->register(SIGUSR1, function($signo) {
        echo "SIGUSR1を受信しました\n";
    });
    
    // エラーが発生するハンドラ
    $handler->register(SIGUSR2, function($signo) {
        throw new RuntimeException("意図的なエラー");
    }, function($exception) {
        echo "エラーをキャッチしました: " . $exception->getMessage() . "\n";
    });
    
    $handler->listHandlers();
    
    echo "\nPID: " . getmypid() . "\n";
    echo "テスト用コマンド:\n";
    echo "  kill -USR1 " . getmypid() . " (正常)\n";
    echo "  kill -USR2 " . getmypid() . " (エラー発生)\n\n";
    
    while (true) {
        sleep(1);
    }
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

実装時のチェックリスト

<?php
class SignalHandlerChecklist {
    /**
     * 環境チェック
     */
    public static function checkEnvironment() {
        $checks = [];
        
        // PHP バージョン
        $checks['php_version'] = [
            'check' => version_compare(PHP_VERSION, '7.1.0', '>='),
            'message' => 'PHP 7.1.0以降',
            'current' => PHP_VERSION
        ];
        
        // CLI環境
        $checks['cli_mode'] = [
            'check' => php_sapi_name() === 'cli',
            'message' => 'CLI環境での実行',
            'current' => php_sapi_name()
        ];
        
        // PCNTL拡張
        $checks['pcntl_extension'] = [
            'check' => extension_loaded('pcntl'),
            'message' => 'PCNTL拡張が有効',
            'current' => extension_loaded('pcntl') ? '有効' : '無効'
        ];
        
        // pcntl_async_signals関数
        $checks['async_signals'] = [
            'check' => function_exists('pcntl_async_signals'),
            'message' => 'pcntl_async_signals関数が利用可能',
            'current' => function_exists('pcntl_async_signals') ? '可能' : '不可'
        ];
        
        // POSIX拡張(オプション)
        $checks['posix_extension'] = [
            'check' => extension_loaded('posix'),
            'message' => 'POSIX拡張が有効(推奨)',
            'current' => extension_loaded('posix') ? '有効' : '無効'
        ];
        
        return $checks;
    }
    
    /**
     * チェック結果を表示
     */
    public static function displayResults() {
        echo "=== 環境チェック結果 ===\n\n";
        
        $checks = self::checkEnvironment();
        $all_passed = true;
        
        foreach ($checks as $name => $check) {
            $status = $check['check'] ? '✓' : '✗';
            $all_passed = $all_passed && $check['check'];
            
            echo "[{$status}] {$check['message']}\n";
            echo "    現在: {$check['current']}\n\n";
        }
        
        echo str_repeat("=", 40) . "\n";
        
        if ($all_passed) {
            echo "✓ すべてのチェックに合格しました\n";
            return true;
        } else {
            echo "✗ 一部のチェックに失敗しました\n";
            return false;
        }
    }
}

// チェック実行
if (SignalHandlerChecklist::displayResults()) {
    echo "\npcntl_async_signalsを安全に使用できます。\n";
} else {
    echo "\n環境を確認してください。\n";
}
?>

まとめ

pcntl_async_signals 関数は、PHP 7.1で導入された革新的な機能で、シグナル処理を大幅に簡素化します。

主な利点:

  • コードの簡潔化: pcntl_signal_dispatch()の明示的な呼び出しが不要
  • リアルタイム性の向上: シグナルが即座に処理される
  • 可読性の向上: メインロジックとシグナル処理の分離が容易

主な用途:

  • グレースフルシャットダウンの実装
  • プロセス間通信の制御
  • 動的な設定リロード
  • パフォーマンス統計のリアルタイム表示
  • ワーカープロセスの管理

重要な注意点:

  • PHP 7.1.0以降が必須
  • CLI環境専用(Web環境では使用不可)
  • リエントラント安全性への配慮が必要
  • シグナルハンドラ内では複雑な処理を避ける
  • エラーハンドリングを適切に実装する

従来の方法との使い分け:

  • 新規プロジェクト: pcntl_async_signals(true)を使用
  • レガシーコード: 互換性のため従来の方法を維持
  • PHP 7.1未満: pcntl_signal_dispatch()を使用

この関数により、PHPでのプロセス制御がより直感的で保守しやすくなりました。特に長時間稼働するCLIアプリケーションやデーモンプロセスの実装において、その真価を発揮します。


PHP 7.1以降の機能を活用して、モダンで堅牢なCLIアプリケーションを構築しましょう。非同期シグナル処理により、より洗練されたプロセス制御が可能になります。

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