[PHP]proc_nice関数の使い方を徹底解説!プロセス優先度の制御をマスター

PHP

PHPスクリプトを実行する際、CPUやシステムリソースの使用を制御したい場合があります。特に、バックグラウンドで重い処理を行う際に、他のプロセスに影響を与えないようにしたいケースです。この記事では、PHPのproc_nice関数について、基本的な使い方から実践的な活用方法まで詳しく解説していきます。

proc_nice関数とは?

proc_niceは、現在実行中のプロセスの優先度(nice値)を変更する関数です。nice値を調整することで、プロセスがCPUリソースを使用する優先順位を制御できます。

基本的な構文

bool proc_nice(int $priority)

パラメータ:

  • $priority: nice値の増減量(-20〜19)
    • 負の値: 優先度を上げる(より多くのCPU時間を割り当て)
    • 正の値: 優先度を下げる(より少ないCPU時間を割り当て)

戻り値:

  • 成功時: true
  • 失敗時: false

重要な制約:

  • UNIX/Linux系システムでのみ利用可能(Windowsでは使用不可)
  • 一般ユーザーは優先度を下げることしかできない
  • 優先度を上げるにはroot権限が必要

nice値の理解

nice値の範囲と意味

nice値優先度説明必要な権限
-20最高最も多くのCPU時間を取得root
-10通常より多くのCPU時間root
0通常デフォルトの優先度
10通常より少ないCPU時間一般ユーザー
19最低最も少ないCPU時間一般ユーザー

nice値の効果

<?php
// 現在のnice値を確認(Linux/Unix)
// コマンドライン: ps -o pid,ni,comm -p [PID]

// 優先度を下げる(バックグラウンドタスクに適している)
proc_nice(10); // nice値を+10する

// 優先度を上げる(root権限が必要)
// proc_nice(-10); // nice値を-10する
?>

基本的な使い方

プロセスの優先度を下げる

<?php
// 重い処理を行う前に優先度を下げる
if (proc_nice(10) === false) {
    echo "優先度の変更に失敗しました\n";
} else {
    echo "優先度を下げました(nice +10)\n";
}

// 重い処理を実行
for ($i = 0; $i < 1000000; $i++) {
    // 計算処理...
}
?>

バックグラウンド処理での使用

<?php
/**
 * バックグラウンドで実行される重い処理
 */
function heavyBackgroundTask() {
    // プロセスの優先度を最低に設定
    proc_nice(19);
    
    echo "バックグラウンドタスク開始(優先度: 最低)\n";
    
    // 重い処理
    $data = [];
    for ($i = 0; $i < 1000000; $i++) {
        $data[] = md5($i);
    }
    
    echo "バックグラウンドタスク完了\n";
    
    return count($data);
}

// 実行
$result = heavyBackgroundTask();
echo "処理した項目数: {$result}\n";
?>

実践的な使用例

1. バッチ処理システム

<?php
/**
 * バッチ処理クラス
 */
class BatchProcessor {
    private $priority = 15; // デフォルトは低優先度
    
    /**
     * 優先度を設定
     */
    public function setPriority($priority) {
        if ($priority < 0 || $priority > 19) {
            throw new InvalidArgumentException('優先度は0〜19の範囲で指定してください');
        }
        $this->priority = $priority;
    }
    
    /**
     * バッチ処理を実行
     */
    public function execute($jobName, callable $job) {
        echo "バッチ処理開始: {$jobName}\n";
        echo "優先度設定: nice +{$this->priority}\n";
        
        // プロセスの優先度を設定
        if (proc_nice($this->priority) === false) {
            echo "警告: 優先度の変更に失敗しました\n";
        }
        
        $startTime = microtime(true);
        $memoryStart = memory_get_usage();
        
        try {
            // ジョブを実行
            $result = $job();
            
            $elapsed = microtime(true) - $startTime;
            $memoryUsed = memory_get_usage() - $memoryStart;
            
            echo "バッチ処理完了: {$jobName}\n";
            echo "実行時間: " . round($elapsed, 2) . "秒\n";
            echo "メモリ使用量: " . round($memoryUsed / 1024 / 1024, 2) . " MB\n";
            
            return [
                'success' => true,
                'result' => $result,
                'elapsed_time' => $elapsed,
                'memory_used' => $memoryUsed
            ];
            
        } catch (Exception $e) {
            echo "エラー: " . $e->getMessage() . "\n";
            
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
    
    /**
     * 複数のジョブを順次実行
     */
    public function executeMultiple(array $jobs) {
        $results = [];
        
        foreach ($jobs as $jobName => $job) {
            echo "\n" . str_repeat("=", 50) . "\n";
            $results[$jobName] = $this->execute($jobName, $job);
        }
        
        return $results;
    }
}

// 使用例
$processor = new BatchProcessor();
$processor->setPriority(15); // 低優先度で実行

$jobs = [
    'データベースバックアップ' => function() {
        sleep(2); // バックアップ処理をシミュレート
        return ['backup_file' => 'backup_' . date('Ymd') . '.sql'];
    },
    
    'ログ集計' => function() {
        $count = 0;
        for ($i = 0; $i < 1000000; $i++) {
            $count++;
        }
        return ['processed_lines' => $count];
    },
    
    'レポート生成' => function() {
        $data = [];
        for ($i = 0; $i < 100000; $i++) {
            $data[] = ['id' => $i, 'value' => rand(1, 100)];
        }
        return ['records' => count($data)];
    }
];

$results = $processor->executeMultiple($jobs);

echo "\n" . str_repeat("=", 50) . "\n";
echo "すべてのバッチ処理が完了しました\n";
?>

2. 画像処理ワーカー

<?php
/**
 * 画像処理ワーカークラス
 */
class ImageProcessingWorker {
    private $inputDir;
    private $outputDir;
    private $priority;
    
    public function __construct($inputDir, $outputDir, $priority = 10) {
        $this->inputDir = $inputDir;
        $this->outputDir = $outputDir;
        $this->priority = $priority;
        
        if (!is_dir($outputDir)) {
            mkdir($outputDir, 0755, true);
        }
    }
    
    /**
     * すべての画像を処理
     */
    public function processAll() {
        // 優先度を設定(CPUを使いすぎないように)
        proc_nice($this->priority);
        
        $files = glob($this->inputDir . '/*.{jpg,jpeg,png,gif}', GLOB_BRACE);
        $total = count($files);
        $processed = 0;
        
        echo "画像処理開始: {$total}枚\n";
        echo "優先度: nice +{$this->priority}\n\n";
        
        foreach ($files as $file) {
            $processed++;
            $filename = basename($file);
            
            echo "処理中 [{$processed}/{$total}]: {$filename}";
            
            $startTime = microtime(true);
            
            try {
                $this->processImage($file);
                $elapsed = microtime(true) - $startTime;
                
                echo " - 完了 (" . round($elapsed, 2) . "秒)\n";
                
            } catch (Exception $e) {
                echo " - エラー: " . $e->getMessage() . "\n";
            }
            
            // 進行状況を表示
            $percent = ($processed / $total) * 100;
            echo "進行状況: " . round($percent, 1) . "%\n\n";
        }
        
        echo "すべての画像処理が完了しました\n";
    }
    
    /**
     * 個別の画像を処理
     */
    private function processImage($inputFile) {
        $filename = basename($inputFile);
        $outputFile = $this->outputDir . '/' . $filename;
        
        // GD関数を使った画像処理(例:リサイズ)
        $imageInfo = getimagesize($inputFile);
        
        if ($imageInfo === false) {
            throw new Exception('画像の読み込みに失敗しました');
        }
        
        list($width, $height, $type) = $imageInfo;
        
        // 画像を読み込む
        switch ($type) {
            case IMAGETYPE_JPEG:
                $source = imagecreatefromjpeg($inputFile);
                break;
            case IMAGETYPE_PNG:
                $source = imagecreatefrompng($inputFile);
                break;
            case IMAGETYPE_GIF:
                $source = imagecreatefromgif($inputFile);
                break;
            default:
                throw new Exception('未対応の画像形式です');
        }
        
        // リサイズ(幅800px)
        $newWidth = 800;
        $newHeight = (int)(($height / $width) * $newWidth);
        
        $resized = imagecreatetruecolor($newWidth, $newHeight);
        imagecopyresampled($resized, $source, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
        
        // 保存
        imagejpeg($resized, $outputFile, 85);
        
        // メモリ解放
        imagedestroy($source);
        imagedestroy($resized);
    }
}

// 使用例
$worker = new ImageProcessingWorker(
    '/path/to/input/images',
    '/path/to/output/images',
    15  // 低優先度で実行
);

try {
    $worker->processAll();
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

3. データエクスポートシステム

<?php
/**
 * 大量データエクスポートクラス
 */
class DataExporter {
    private $db;
    private $chunkSize = 1000;
    
    public function __construct(PDO $db) {
        $this->db = $db;
    }
    
    /**
     * CSVエクスポート(低優先度で実行)
     */
    public function exportToCSV($table, $outputFile, $priority = 15) {
        echo "データエクスポート開始\n";
        echo "テーブル: {$table}\n";
        echo "出力ファイル: {$outputFile}\n";
        
        // プロセスの優先度を下げる
        if (proc_nice($priority)) {
            echo "優先度: nice +{$priority}\n";
        }
        
        $startTime = microtime(true);
        
        // 総レコード数を取得
        $stmt = $this->db->query("SELECT COUNT(*) FROM {$table}");
        $totalRows = $stmt->fetchColumn();
        
        echo "総レコード数: {$totalRows}\n\n";
        
        // CSVファイルを開く
        $fp = fopen($outputFile, 'w');
        
        if ($fp === false) {
            throw new Exception('ファイルを開けませんでした');
        }
        
        $offset = 0;
        $exportedRows = 0;
        
        // チャンクごとに処理
        while ($offset < $totalRows) {
            $stmt = $this->db->prepare(
                "SELECT * FROM {$table} LIMIT :limit OFFSET :offset"
            );
            $stmt->bindValue(':limit', $this->chunkSize, PDO::PARAM_INT);
            $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
            $stmt->execute();
            
            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
            
            // ヘッダーを書き込む(最初のチャンクのみ)
            if ($offset === 0 && !empty($rows)) {
                fputcsv($fp, array_keys($rows[0]));
            }
            
            // データを書き込む
            foreach ($rows as $row) {
                fputcsv($fp, $row);
                $exportedRows++;
            }
            
            $offset += $this->chunkSize;
            
            // 進行状況を表示
            $percent = min(100, ($exportedRows / $totalRows) * 100);
            printf("\r進行状況: %.1f%% (%d/%d)", $percent, $exportedRows, $totalRows);
            flush();
            
            // CPUを占有しないように少し待機
            usleep(10000); // 0.01秒
        }
        
        fclose($fp);
        
        $elapsed = microtime(true) - $startTime;
        $fileSize = filesize($outputFile);
        
        echo "\n\nエクスポート完了\n";
        echo "エクスポートレコード数: {$exportedRows}\n";
        echo "ファイルサイズ: " . round($fileSize / 1024 / 1024, 2) . " MB\n";
        echo "実行時間: " . round($elapsed, 2) . "秒\n";
        
        return [
            'rows' => $exportedRows,
            'file_size' => $fileSize,
            'elapsed_time' => $elapsed
        ];
    }
}

// 使用例
try {
    $pdo = new PDO('mysql:host=localhost;dbname=mydb', 'username', 'password');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
    $exporter = new DataExporter($pdo);
    
    // 低優先度でエクスポート
    $result = $exporter->exportToCSV(
        'users',
        'users_export_' . date('Ymd_His') . '.csv',
        15
    );
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

4. cron実行スクリプト

<?php
/**
 * Cron実行用の基底クラス
 */
abstract class CronJob {
    protected $name;
    protected $priority = 10;
    protected $logFile;
    
    public function __construct($name, $priority = 10) {
        $this->name = $name;
        $this->priority = $priority;
        $this->logFile = "/var/log/cron/{$name}.log";
    }
    
    /**
     * ジョブを実行
     */
    public function run() {
        $this->log("=== Cronジョブ開始: {$this->name} ===");
        $this->log("日時: " . date('Y-m-d H:i:s'));
        
        // プロセスの優先度を設定
        if (proc_nice($this->priority)) {
            $this->log("優先度: nice +{$this->priority}");
        } else {
            $this->log("警告: 優先度の変更に失敗");
        }
        
        $startTime = microtime(true);
        
        try {
            // 実際の処理を実行
            $result = $this->execute();
            
            $elapsed = microtime(true) - $startTime;
            
            $this->log("実行完了");
            $this->log("実行時間: " . round($elapsed, 2) . "秒");
            
            if ($result !== null) {
                $this->log("結果: " . json_encode($result));
            }
            
            return true;
            
        } catch (Exception $e) {
            $this->log("エラー: " . $e->getMessage());
            $this->log("スタックトレース: " . $e->getTraceAsString());
            
            return false;
        } finally {
            $this->log("=== Cronジョブ終了 ===\n");
        }
    }
    
    /**
     * 実際の処理(サブクラスで実装)
     */
    abstract protected function execute();
    
    /**
     * ログに記録
     */
    protected function log($message) {
        $logDir = dirname($this->logFile);
        
        if (!is_dir($logDir)) {
            mkdir($logDir, 0755, true);
        }
        
        $timestamp = date('Y-m-d H:i:s');
        file_put_contents($this->logFile, "[{$timestamp}] {$message}\n", FILE_APPEND);
        echo "[{$timestamp}] {$message}\n";
    }
}

/**
 * データベースクリーンアップジョブ
 */
class DatabaseCleanupJob extends CronJob {
    private $db;
    
    public function __construct(PDO $db) {
        parent::__construct('database_cleanup', 15);
        $this->db = $db;
    }
    
    protected function execute() {
        $this->log("古いログを削除中...");
        
        // 30日より古いログを削除
        $stmt = $this->db->prepare(
            "DELETE FROM logs WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY)"
        );
        $stmt->execute();
        
        $deletedRows = $stmt->rowCount();
        $this->log("削除したログ: {$deletedRows}件");
        
        // テーブルの最適化
        $this->log("テーブルを最適化中...");
        $this->db->exec("OPTIMIZE TABLE logs");
        
        return ['deleted_rows' => $deletedRows];
    }
}

/**
 * キャッシュクリアジョブ
 */
class CacheClearJob extends CronJob {
    private $cacheDir;
    
    public function __construct($cacheDir) {
        parent::__construct('cache_clear', 15);
        $this->cacheDir = $cacheDir;
    }
    
    protected function execute() {
        $this->log("キャッシュをクリア中: {$this->cacheDir}");
        
        $files = glob($this->cacheDir . '/*');
        $deletedCount = 0;
        
        foreach ($files as $file) {
            if (is_file($file)) {
                unlink($file);
                $deletedCount++;
            }
        }
        
        $this->log("削除したファイル: {$deletedCount}件");
        
        return ['deleted_files' => $deletedCount];
    }
}

// 使用例(cronから実行されるスクリプト)
try {
    // データベース接続
    $pdo = new PDO('mysql:host=localhost;dbname=mydb', 'username', 'password');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
    // ジョブを実行
    $jobs = [
        new DatabaseCleanupJob($pdo),
        new CacheClearJob('/tmp/cache')
    ];
    
    foreach ($jobs as $job) {
        $job->run();
        echo "\n";
    }
    
} catch (Exception $e) {
    echo "致命的エラー: " . $e->getMessage() . "\n";
    exit(1);
}
?>

環境による制約と対処法

Windowsでの代替手段

<?php
/**
 * クロスプラットフォーム対応の優先度設定
 */
class ProcessPriority {
    
    /**
     * プロセスの優先度を設定(プラットフォーム対応)
     */
    public static function setLow() {
        if (PHP_OS_FAMILY === 'Windows') {
            // Windowsの場合は警告のみ
            echo "警告: Windowsではproc_niceは使用できません\n";
            return false;
        }
        
        // Unix/Linuxの場合
        return proc_nice(15);
    }
    
    /**
     * 現在のプラットフォームを確認
     */
    public static function getPlatformInfo() {
        return [
            'os_family' => PHP_OS_FAMILY,
            'os' => PHP_OS,
            'proc_nice_available' => function_exists('proc_nice') && PHP_OS_FAMILY !== 'Windows'
        ];
    }
}

// 使用例
$info = ProcessPriority::getPlatformInfo();

if ($info['proc_nice_available']) {
    echo "proc_niceが利用可能です\n";
    ProcessPriority::setLow();
} else {
    echo "このプラットフォームではproc_niceは使用できません\n";
    echo "OS: {$info['os']}\n";
}
?>

よくある間違いと注意点

間違い1: Windowsで使用しようとする

<?php
// ❌ Windowsでは動作しない
if (PHP_OS_FAMILY === 'Windows') {
    proc_nice(10); // 何も起こらない、またはエラー
}

// ✅ プラットフォームをチェック
if (function_exists('proc_nice') && PHP_OS_FAMILY !== 'Windows') {
    proc_nice(10);
}
?>

間違い2: 優先度を上げようとする(一般ユーザー)

<?php
// ❌ 一般ユーザーは優先度を上げられない
proc_nice(-10); // 失敗する

// ✅ 優先度を下げることはできる
proc_nice(10); // 成功
?>

間違い3: 戻り値をチェックしない

<?php
// ❌ 失敗を検出できない
proc_nice(10);

// ✅ 戻り値をチェック
if (proc_nice(10) === false) {
    echo "優先度の変更に失敗しました\n";
}
?>

まとめ

proc_nice関数は、PHPでプロセスの優先度を制御するための重要な関数です。以下のポイントを押さえておきましょう。

  • CPUリソースの使用を制御できる
  • バックグラウンド処理を低優先度で実行
  • 一般ユーザーは優先度を下げることのみ可能
  • Unix/Linux系システムでのみ利用可能
  • nice値は**-20〜19**の範囲(数値が大きいほど優先度が低い)
  • バッチ処理やcronジョブに最適
  • 重い処理を実行する際は必ず優先度を下げる
  • Windowsでは使用不可なので、プラットフォームをチェック

proc_niceを適切に使用することで、システムリソースを効率的に管理し、他のプロセスに影響を与えずに重い処理を実行できます。特にバックグラウンド処理や定期実行タスクでは必須のテクニックです!

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