はじめに
長時間実行される処理や、外部APIとの通信など、PHPスクリプトで「一定時間経過したら処理を中断したい」という場面に遭遇することがあります。
そんな時に役立つのが pcntl_alarm
関数です。この記事では、UNIXシグナルを使ったタイマー機能を実装するこの関数について、基礎から実践的な活用方法まで詳しく解説します。
pcntl_alarm関数とは
pcntl_alarm
は、指定された秒数後にSIGALRMシグナルを送信するタイマーを設定する関数です。プロセス制御拡張(PCNTL)の一部で、主にCLI環境で使用されます。
基本構文
int pcntl_alarm(int $seconds)
パラメータ
- $seconds (int): アラームを発生させるまでの秒数(0を指定するとアラームをキャンセル)
戻り値
- int: 前回設定されていたアラームの残り秒数(設定されていない場合は0)
重要な前提条件
<?php
// 利用可能性のチェック
if (!function_exists('pcntl_alarm')) {
die("pcntl拡張が利用できません。CLI環境で実行してください。\n");
}
// この関数はCLI(コマンドライン)環境でのみ動作します
if (php_sapi_name() !== 'cli') {
die("この機能はCLI環境でのみ利用可能です。\n");
}
?>
基本的な使用例
シンプルなアラーム設定
<?php
// シグナルハンドラの定義
function alarmHandler($signo) {
echo "\n⏰ アラームが発生しました!\n";
exit(0);
}
// シグナルハンドラを登録
pcntl_signal(SIGALRM, 'alarmHandler');
echo "3秒後にアラームを設定します...\n";
// 3秒後のアラームを設定
pcntl_alarm(3);
// シグナル待機
while (true) {
pcntl_signal_dispatch(); // シグナルをディスパッチ
echo ".";
sleep(1);
}
?>
アラームのキャンセル
<?php
function alarmHandler($signo) {
echo "アラーム発生!\n";
}
pcntl_signal(SIGALRM, 'alarmHandler');
// 5秒後のアラームを設定
$remaining = pcntl_alarm(5);
echo "5秒後のアラームを設定しました\n";
sleep(2);
// アラームをキャンセル(0を設定)
$remaining = pcntl_alarm(0);
echo "アラームをキャンセルしました(残り{$remaining}秒)\n";
sleep(5);
echo "プログラム終了(アラームは発生しませんでした)\n";
?>
実践的な活用例
1. タイムアウト付き処理の実装
<?php
class TimeoutExecutor {
private $timeout_occurred = false;
private $original_handler = null;
/**
* タイムアウト付きで関数を実行
* @param callable $callback 実行する関数
* @param int $timeout タイムアウト秒数
* @return mixed 実行結果、またはnull(タイムアウト時)
*/
public function execute($callback, $timeout) {
$this->timeout_occurred = false;
// シグナルハンドラを設定
$this->original_handler = pcntl_signal_get_handler(SIGALRM);
pcntl_signal(SIGALRM, [$this, 'handleTimeout']);
// アラーム設定
pcntl_alarm($timeout);
$result = null;
try {
// コールバック実行
$result = call_user_func($callback);
// 正常終了したらアラームをキャンセル
pcntl_alarm(0);
} catch (Exception $e) {
pcntl_alarm(0);
throw $e;
} finally {
// 元のハンドラを復元
if ($this->original_handler !== null) {
pcntl_signal(SIGALRM, $this->original_handler);
}
}
return $this->timeout_occurred ? null : $result;
}
/**
* タイムアウトハンドラ
*/
public function handleTimeout($signo) {
$this->timeout_occurred = true;
throw new RuntimeException("処理がタイムアウトしました");
}
/**
* タイムアウトが発生したかチェック
*/
public function hasTimedOut() {
return $this->timeout_occurred;
}
}
// 使用例
$executor = new TimeoutExecutor();
// 正常に終了する処理
echo "=== ケース1: 正常終了 ===\n";
$result = $executor->execute(function() {
echo "処理開始...\n";
sleep(2);
echo "処理完了\n";
return "成功";
}, 5);
echo "結果: " . ($result ?? "タイムアウト") . "\n\n";
// タイムアウトする処理
echo "=== ケース2: タイムアウト ===\n";
try {
$result = $executor->execute(function() {
echo "重い処理開始...\n";
for ($i = 0; $i < 10; $i++) {
echo "処理中... {$i}\n";
sleep(1);
pcntl_signal_dispatch();
}
return "完了";
}, 3);
echo "結果: " . ($result ?? "タイムアウト") . "\n";
} catch (RuntimeException $e) {
echo "エラー: " . $e->getMessage() . "\n";
}
?>
2. 外部コマンド実行のタイムアウト管理
<?php
class CommandExecutor {
private $timeout = 30;
private $timed_out = false;
/**
* コマンドをタイムアウト付きで実行
* @param string $command 実行するコマンド
* @param int $timeout タイムアウト秒数
* @return array 実行結果
*/
public function executeWithTimeout($command, $timeout = 30) {
$this->timeout = $timeout;
$this->timed_out = false;
// シグナルハンドラ設定
pcntl_signal(SIGALRM, [$this, 'handleAlarm']);
pcntl_alarm($timeout);
$start_time = microtime(true);
$output = [];
$return_code = 0;
try {
// コマンド実行
exec($command, $output, $return_code);
// 正常終了
pcntl_alarm(0);
} catch (Exception $e) {
pcntl_alarm(0);
throw $e;
}
$execution_time = microtime(true) - $start_time;
return [
'success' => !$this->timed_out && $return_code === 0,
'timed_out' => $this->timed_out,
'output' => $output,
'return_code' => $return_code,
'execution_time' => $execution_time,
'timeout_setting' => $timeout
];
}
/**
* アラームハンドラ
*/
public function handleAlarm($signo) {
$this->timed_out = true;
throw new RuntimeException("コマンド実行がタイムアウトしました({$this->timeout}秒)");
}
}
// 使用例
$executor = new CommandExecutor();
// 素早く終わるコマンド
echo "=== 高速コマンド ===\n";
$result = $executor->executeWithTimeout('echo "Hello, World!"', 5);
echo "成功: " . ($result['success'] ? 'Yes' : 'No') . "\n";
echo "実行時間: " . number_format($result['execution_time'], 4) . "秒\n";
echo "出力: " . implode("\n", $result['output']) . "\n\n";
// 長時間かかるコマンド(タイムアウトテスト)
echo "=== 長時間コマンド(タイムアウトテスト) ===\n";
try {
$result = $executor->executeWithTimeout('sleep 10', 3);
echo "タイムアウト: " . ($result['timed_out'] ? 'Yes' : 'No') . "\n";
} catch (RuntimeException $e) {
echo "エラー: " . $e->getMessage() . "\n";
}
?>
3. データベースクエリのタイムアウト監視
<?php
class DatabaseQueryMonitor {
private $connection;
private $max_query_time = 30;
public function __construct($connection, $max_query_time = 30) {
$this->connection = $connection;
$this->max_query_time = $max_query_time;
}
/**
* タイムアウト監視付きクエリ実行
*/
public function queryWithTimeout($sql, $params = [], $timeout = null) {
$timeout = $timeout ?? $this->max_query_time;
$query_timed_out = false;
// アラームハンドラ設定
pcntl_signal(SIGALRM, function($signo) use (&$query_timed_out, $sql) {
$query_timed_out = true;
error_log("クエリタイムアウト: " . substr($sql, 0, 100));
});
pcntl_alarm($timeout);
$start_time = microtime(true);
$result = null;
$error = null;
try {
// クエリ実行(シミュレーション)
$result = $this->executeQuery($sql, $params);
pcntl_alarm(0);
} catch (Exception $e) {
pcntl_alarm(0);
$error = $e->getMessage();
}
$execution_time = microtime(true) - $start_time;
// ログ記録
$this->logQuery([
'sql' => $sql,
'params' => $params,
'execution_time' => $execution_time,
'timed_out' => $query_timed_out,
'success' => !$query_timed_out && $error === null,
'error' => $error
]);
if ($query_timed_out) {
throw new RuntimeException("クエリがタイムアウトしました({$timeout}秒)");
}
if ($error !== null) {
throw new RuntimeException("クエリエラー: {$error}");
}
return $result;
}
/**
* クエリ実行(シミュレーション)
*/
private function executeQuery($sql, $params) {
// 実際のデータベース処理をシミュレート
echo "クエリ実行中: " . substr($sql, 0, 50) . "...\n";
// ランダムな処理時間
$processing_time = rand(1, 5);
for ($i = 0; $i < $processing_time; $i++) {
sleep(1);
pcntl_signal_dispatch();
echo ".";
}
echo "\n";
return ['rows' => rand(0, 100)];
}
/**
* クエリログ記録
*/
private function logQuery($log_data) {
$status = $log_data['success'] ? '✓' : '✗';
$time = number_format($log_data['execution_time'], 4);
echo sprintf(
"[%s] クエリ実行時間: %ss | タイムアウト: %s | SQL: %s\n",
$status,
$time,
$log_data['timed_out'] ? 'Yes' : 'No',
substr($log_data['sql'], 0, 50)
);
}
}
// 使用例
echo "=== データベースクエリ監視 ===\n\n";
$monitor = new DatabaseQueryMonitor(null, 3);
// 通常のクエリ
try {
echo "ケース1: 通常クエリ\n";
$result = $monitor->queryWithTimeout("SELECT * FROM users WHERE id = ?", [123]);
echo "結果: 成功\n\n";
} catch (RuntimeException $e) {
echo "エラー: " . $e->getMessage() . "\n\n";
}
// 長時間クエリ
try {
echo "ケース2: 長時間クエリ(タイムアウト予想)\n";
$result = $monitor->queryWithTimeout(
"SELECT * FROM large_table JOIN another_large_table...",
[]
);
echo "結果: 成功\n\n";
} catch (RuntimeException $e) {
echo "エラー: " . $e->getMessage() . "\n\n";
}
?>
4. バックグラウンドジョブの監視
<?php
class BackgroundJobMonitor {
private $jobs = [];
private $max_execution_time = 300; // 5分
/**
* ジョブを登録して実行
*/
public function registerJob($job_id, $callback, $timeout = null) {
$timeout = $timeout ?? $this->max_execution_time;
$this->jobs[$job_id] = [
'id' => $job_id,
'callback' => $callback,
'timeout' => $timeout,
'start_time' => time(),
'status' => 'running',
'result' => null
];
return $this->executeJob($job_id);
}
/**
* ジョブ実行
*/
private function executeJob($job_id) {
$job = &$this->jobs[$job_id];
// タイムアウトハンドラ設定
pcntl_signal(SIGALRM, function($signo) use (&$job) {
$job['status'] = 'timeout';
$job['end_time'] = time();
throw new RuntimeException("ジョブ {$job['id']} がタイムアウトしました");
});
pcntl_alarm($job['timeout']);
try {
echo "[{$job_id}] ジョブ開始(タイムアウト: {$job['timeout']}秒)\n";
$result = call_user_func($job['callback']);
pcntl_alarm(0);
$job['status'] = 'completed';
$job['result'] = $result;
$job['end_time'] = time();
$duration = $job['end_time'] - $job['start_time'];
echo "[{$job_id}] ジョブ完了(実行時間: {$duration}秒)\n";
return $result;
} catch (RuntimeException $e) {
pcntl_alarm(0);
echo "[{$job_id}] " . $e->getMessage() . "\n";
throw $e;
}
}
/**
* ジョブステータス取得
*/
public function getJobStatus($job_id) {
if (!isset($this->jobs[$job_id])) {
return null;
}
$job = $this->jobs[$job_id];
$duration = isset($job['end_time'])
? $job['end_time'] - $job['start_time']
: time() - $job['start_time'];
return [
'id' => $job['id'],
'status' => $job['status'],
'duration' => $duration,
'timeout' => $job['timeout'],
'result' => $job['result']
];
}
/**
* 全ジョブのサマリー表示
*/
public function displaySummary() {
echo "\n=== ジョブ実行サマリー ===\n";
foreach ($this->jobs as $job) {
$duration = isset($job['end_time'])
? $job['end_time'] - $job['start_time']
: time() - $job['start_time'];
$status_icon = $job['status'] === 'completed' ? '✓' :
($job['status'] === 'timeout' ? '⏱' : '⋯');
echo sprintf(
"[%s] ジョブID: %s | ステータス: %s | 実行時間: %ds / %ds\n",
$status_icon,
$job['id'],
$job['status'],
$duration,
$job['timeout']
);
}
}
}
// 使用例
$monitor = new BackgroundJobMonitor();
// ジョブ1: 正常終了
try {
$monitor->registerJob('job1', function() {
echo " データ処理中...\n";
sleep(3);
return ['processed' => 100];
}, 10);
} catch (RuntimeException $e) {
echo " エラー: " . $e->getMessage() . "\n";
}
// ジョブ2: タイムアウト
try {
$monitor->registerJob('job2', function() {
echo " 重い処理中...\n";
for ($i = 0; $i < 10; $i++) {
sleep(1);
echo " 進捗: " . ($i + 1) * 10 . "%\n";
pcntl_signal_dispatch();
}
return ['processed' => 1000];
}, 5);
} catch (RuntimeException $e) {
echo " エラー: " . $e->getMessage() . "\n";
}
// ジョブ3: 高速処理
try {
$monitor->registerJob('job3', function() {
echo " 高速処理中...\n";
sleep(1);
return ['processed' => 50];
}, 10);
} catch (RuntimeException $e) {
echo " エラー: " . $e->getMessage() . "\n";
}
$monitor->displaySummary();
?>
重要な注意点と制限事項
マルチスレッド環境での注意
<?php
/**
* pcntl_alarmの制限事項
*/
// 1. プロセスごとに1つのアラームのみ
echo "=== 制限1: 1つのアラームのみ ===\n";
pcntl_signal(SIGALRM, function($signo) {
echo "アラーム発生\n";
});
// 最初のアラーム設定
$remaining1 = pcntl_alarm(5);
echo "5秒後のアラームを設定\n";
sleep(1);
// 2つ目のアラームを設定すると、1つ目は上書きされる
$remaining2 = pcntl_alarm(3);
echo "3秒後のアラームを設定(前回の残り: {$remaining2}秒)\n";
// 2. sleep()との競合
echo "\n=== 制限2: sleep()との競合 ===\n";
pcntl_alarm(2);
echo "2秒のアラームを設定してから5秒スリープ\n";
sleep(5); // sleep中にアラームが発生
echo "スリープ終了\n";
// 3. Web環境では動作しない
echo "\n=== 制限3: CLI環境のみ ===\n";
echo "現在の実行環境: " . php_sapi_name() . "\n";
if (php_sapi_name() !== 'cli') {
echo "⚠️ この関数はWeb環境では動作しません\n";
}
?>
ベストプラクティス
<?php
class AlarmBestPractices {
/**
* 安全なアラーム設定
*/
public static function safeAlarmSetup() {
// 1. 環境チェック
if (php_sapi_name() !== 'cli') {
throw new RuntimeException("CLI環境が必要です");
}
// 2. PCNTL拡張チェック
if (!extension_loaded('pcntl')) {
throw new RuntimeException("PCNTL拡張が必要です");
}
// 3. シグナルが有効か確認
if (!function_exists('pcntl_signal')) {
throw new RuntimeException("シグナル機能が利用できません");
}
return true;
}
/**
* ネストしたアラームの管理
*/
public static function nestedAlarmExample() {
$alarm_stack = [];
// 外側のアラーム
pcntl_signal(SIGALRM, function($signo) {
echo "外側のアラーム発生\n";
});
$alarm_stack[] = pcntl_alarm(10);
echo "外側アラーム設定: 10秒\n";
sleep(2);
// 内側のアラーム(上書き)
$remaining = pcntl_alarm(3);
$alarm_stack[] = $remaining;
echo "内側アラーム設定: 3秒(前回の残り: {$remaining}秒)\n";
// 内側の処理完了後、外側のアラームを復元
sleep(1);
pcntl_alarm(0); // キャンセル
// スタックから復元(簡易実装)
$previous = array_pop($alarm_stack);
if ($previous > 0) {
pcntl_alarm($previous - 3); // 経過時間を考慮
echo "外側のアラームを復元\n";
}
}
/**
* シグナルディスパッチの重要性
*/
public static function signalDispatchExample() {
$counter = 0;
pcntl_signal(SIGALRM, function($signo) use (&$counter) {
echo "\nアラーム発生!(カウント: {$counter})\n";
exit(0);
});
pcntl_alarm(3);
echo "pcntl_signal_dispatch()を呼ばないとシグナルが処理されません\n";
while (true) {
// シグナルディスパッチが必要
pcntl_signal_dispatch();
echo ".";
$counter++;
usleep(100000); // 0.1秒
}
}
}
// 実行例
try {
AlarmBestPractices::safeAlarmSetup();
echo "✓ 環境チェック完了\n\n";
echo "=== ネストしたアラーム例 ===\n";
AlarmBestPractices::nestedAlarmExample();
} catch (RuntimeException $e) {
echo "エラー: " . $e->getMessage() . "\n";
}
?>
代替手段との比較
<?php
/**
* pcntl_alarm の代替手段
*/
// 代替1: set_time_limit(Web環境でも使用可能)
echo "=== set_time_limit ===\n";
set_time_limit(5); // スクリプト全体の実行時間制限
echo "スクリプト実行時間を5秒に制限\n";
// 代替2: stream_set_timeout(ストリーム操作)
echo "\n=== stream_set_timeout ===\n";
$fp = fsockopen("example.com", 80, $errno, $errstr, 30);
if ($fp) {
stream_set_timeout($fp, 5); // 5秒のタイムアウト
echo "ストリームタイムアウトを5秒に設定\n";
fclose($fp);
}
// 代替3: curl_setopt(HTTP通信)
echo "\n=== curl タイムアウト ===\n";
if (function_exists('curl_init')) {
$ch = curl_init("https://example.com");
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
echo "cURLタイムアウトを設定\n";
curl_close($ch);
}
// 比較表
echo "\n=== 各手法の比較 ===\n";
$comparison = [
'pcntl_alarm' => [
'環境' => 'CLI専用',
'精度' => '秒単位',
'用途' => '任意の処理のタイムアウト',
'複雑さ' => '高(シグナル処理が必要)'
],
'set_time_limit' => [
'環境' => 'Web/CLI両方',
'精度' => '秒単位',
'用途' => 'スクリプト全体の制限',
'複雑さ' => '低'
],
'stream_set_timeout' => [
'環境' => 'Web/CLI両方',
'精度' => '秒+マイクロ秒',
'用途' => 'ストリーム操作',
'複雑さ' => '中'
],
'curl timeout' => [
'環境' => 'Web/CLI両方',
'精度' => '秒単位',
'用途' => 'HTTP通信',
'複雑さ' => '低'
]
];
foreach ($comparison as $method => $details) {
echo "\n{$method}:\n";
foreach ($details as $key => $value) {
echo " {$key}: {$value}\n";
}
}
?>
まとめ
pcntl_alarm
関数は、CLI環境でのタイムアウト処理を実装する強力なツールです。
主な特徴:
- UNIXシグナルベース: 低レベルなタイマー機能
- 柔軟な制御: 任意の処理にタイムアウトを設定可能
- CLI専用: コマンドライン環境での使用に特化
主な用途:
- 長時間実行される可能性のある処理の監視
- 外部コマンド実行のタイムアウト管理
- バックグラウンドジョブの実行時間制限
- データベースクエリの監視
重要な注意点:
- CLI環境でのみ動作(Web環境では使用不可)
- プロセスごとに1つのアラームのみ設定可能
- シグナルディスパッチ(
pcntl_signal_dispatch()
)の呼び出しが必須 - Web環境では
set_time_limit
やstream_set_timeout
などの代替手段を使用
適切に使用することで、PHPのバッチ処理やCLIツールにおいて、堅牢なタイムアウト管理を実現できます。ただし、シグナル処理の理解が必要なため、使用前に十分な検証を行うことをお勧めします。
プロセス制御の理解は、PHPでの高度なシステムプログラミングへの第一歩です。この記事が、より堅牢なCLIアプリケーション開発の一助となれば幸いです。