[PHP]sleep()完全解説|スクリプト実行の一時停止と待機処理の実践パターン

PHP

はじめに

PHPスクリプトの実行を一定時間一時停止したい場面はよくあります。APIのレート制限対策・リトライ処理の間隔確保・ポーリング処理・バッチ処理の負荷分散など、「少し待ってから次の処理へ」というニーズは非常に多様です。

sleep() はスクリプトの実行を指定した秒数だけ停止する関数です。シンプルですが、usleep()(マイクロ秒)・time_nanosleep()(ナノ秒)・time_sleep_until()(指定時刻まで)といった関連関数と組み合わせることで、精度の高い待機制御が実現できます。


関数の概要

項目内容
関数名sleep()
所属PHP 時間関数
導入バージョンPHP 4以降
PHP 8.x対応済み

構文

sleep(int $seconds): int

パラメータ

パラメータ説明
$secondsint停止する秒数(0以上の整数)

戻り値

戻り値意味
0正常に指定秒数だけ停止した
正の整数シグナルによって中断された場合、残り秒数を返す

⚠️ $seconds に負の値を渡すと ValueError が発生します(PHP 8.0以降)。


待機精度別の関連関数一覧

関数精度引数用途
sleep(int $seconds)秒数(int)秒単位の待機
usleep(int $microseconds)マイクロ秒マイクロ秒(int)ミリ秒・マイクロ秒単位の待機
time_nanosleep(int $sec, int $nanosec)ナノ秒秒 + ナノ秒ナノ秒精度の待機
time_sleep_until(float $timestamp)マイクロ秒Unixタイムスタンプ指定時刻まで待機

換算表

1秒 = 1,000 ミリ秒 = 1,000,000 マイクロ秒 = 1,000,000,000 ナノ秒

sleep(1)              → 1秒
usleep(1_000_000)     → 1秒
usleep(500_000)       → 0.5秒(500ミリ秒)
usleep(100_000)       → 0.1秒(100ミリ秒)
usleep(1_000)         → 0.001秒(1ミリ秒)

基本的な使い方

<?php
// 3秒間停止する
echo "処理開始\n";
sleep(3);
echo "3秒後\n";

// usleep で 500ミリ秒待機
usleep(500_000);
echo "500ms後\n";

// 指定時刻まで待機
$target = microtime(true) + 2.5; // 2.5秒後
time_sleep_until($target);
echo "2.5秒後\n";

実践的なクラスベースの使用例

例1:APIレート制限対応リトライクラス

<?php
/**
 * APIレート制限に対応したリトライ処理クラス
 * 指数バックオフ(Exponential Backoff)で待機時間を徐々に延ばす
 */
class ExponentialBackoffRetry
{
    public function __construct(
        private readonly int   $maxAttempts    = 5,
        private readonly float $baseDelaySeconds = 1.0,
        private readonly float $maxDelaySeconds  = 60.0,
        private readonly float $multiplier       = 2.0,
        private readonly float $jitterFactor     = 0.1
    ) {}

    /**
     * コールバックをリトライ付きで実行する
     *
     * @param callable(): mixed  $operation   実行する処理
     * @param callable(\Throwable): bool $retryOn どの例外でリトライするか
     */
    public function run(callable $operation, ?callable $retryOn = null): mixed
    {
        $retryOn ??= fn(\Throwable $e) => true;
        $attempt  = 0;

        while (true) {
            $attempt++;
            try {
                $result = $operation();
                if ($attempt > 1) {
                    echo "成功({$attempt}回目)\n";
                }
                return $result;

            } catch (\Throwable $e) {
                if ($attempt >= $this->maxAttempts || !$retryOn($e)) {
                    throw $e;
                }

                $delay = $this->calcDelay($attempt);
                echo "失敗({$attempt}回目): {$e->getMessage()}\n";
                echo "  {$delay}秒後にリトライ...\n";

                // usleep でミリ秒精度の待機
                usleep((int) ($delay * 1_000_000));
            }
        }
    }

    /**
     * 指数バックオフ + ジッター(揺らぎ)で待機時間を計算する
     * ジッターを加えることで複数クライアントの同時リトライを分散する
     */
    private function calcDelay(int $attempt): float
    {
        $base   = $this->baseDelaySeconds * ($this->multiplier ** ($attempt - 1));
        $capped = min($base, $this->maxDelaySeconds);
        // ±jitterFactor の揺らぎを追加
        $jitter = $capped * $this->jitterFactor * (mt_rand(-100, 100) / 100);
        return max(0.1, $capped + $jitter);
    }
}

$retry = new ExponentialBackoffRetry(
    maxAttempts:     4,
    baseDelaySeconds: 1.0,
    multiplier:       2.0
);

$callCount = 0;
try {
    $result = $retry->run(
        operation: function () use (&$callCount): string {
            $callCount++;
            if ($callCount < 3) {
                throw new \RuntimeException("APIレート制限(429)");
            }
            return "成功レスポンス";
        },
        retryOn: fn(\Throwable $e) => str_contains($e->getMessage(), '429')
    );
    echo "結果: {$result}\n";
} catch (\Throwable $e) {
    echo "最終失敗: {$e->getMessage()}\n";
}

/*
出力例:
失敗(1回目): APIレート制限(429)
  1.0秒後にリトライ...
失敗(2回目): APIレート制限(429)
  2.0秒後にリトライ...
成功(3回目)
結果: 成功レスポンス
*/

例2:ポーリング処理クラス

<?php
/**
 * 非同期ジョブの完了を定期的にポーリングするクラス
 * sleep() で一定間隔を空けながらステータスを確認する
 */
class JobPoller
{
    public function __construct(
        private readonly int   $intervalSeconds = 2,
        private readonly int   $timeoutSeconds  = 60,
        private readonly bool  $verbose         = true
    ) {}

    /**
     * 条件が満たされるまでポーリングを続ける
     *
     * @param callable(): mixed       $checker  ステータス確認処理(完了時に非null を返す)
     * @param callable(mixed): bool   $isDone   完了判定
     */
    public function waitUntil(callable $checker, callable $isDone): mixed
    {
        $startTime = time();
        $attempt   = 0;

        while (true) {
            $attempt++;
            $elapsed = time() - $startTime;

            if ($elapsed >= $this->timeoutSeconds) {
                throw new \RuntimeException(
                    "タイムアウト: {$this->timeoutSeconds}秒以内に完了しませんでした"
                );
            }

            $status = $checker();

            if ($this->verbose) {
                $statusStr = is_array($status)
                    ? json_encode($status, JSON_UNESCAPED_UNICODE)
                    : (string) $status;
                echo "[{$elapsed}秒経過 / 試行{$attempt}回] ステータス: {$statusStr}\n";
            }

            if ($isDone($status)) {
                echo "完了({$elapsed}秒 / {$attempt}回のポーリング)\n";
                return $status;
            }

            // タイムアウトまでの残り時間と通常インターバルの小さい方だけ待つ
            $remaining  = $this->timeoutSeconds - $elapsed;
            $waitSeconds = min($this->intervalSeconds, $remaining);

            if ($waitSeconds > 0) {
                sleep((int) $waitSeconds);
            }
        }
    }
}

// 擬似的な非同期ジョブのステータスチェック
$jobStartedAt = time();
$poller = new JobPoller(intervalSeconds: 1, timeoutSeconds: 10);

try {
    $result = $poller->waitUntil(
        checker: function () use ($jobStartedAt): array {
            $elapsed = time() - $jobStartedAt;
            // 3秒後に完了するシミュレーション
            return [
                'status'   => $elapsed >= 3 ? 'completed' : 'running',
                'progress' => min(100, $elapsed * 33),
            ];
        },
        isDone: fn(array $s) => $s['status'] === 'completed'
    );
    echo "ジョブ完了: " . json_encode($result, JSON_UNESCAPED_UNICODE) . "\n";
} catch (\RuntimeException $e) {
    echo "エラー: {$e->getMessage()}\n";
}

例3:レート制限付きバッチ処理クラス

<?php
/**
 * 大量データのバッチ処理でAPIや DB への負荷を制御するクラス
 * sleep() / usleep() で処理間隔を設けてレート制限を守る
 */
class RateLimitedBatchProcessor
{
    private int   $processedCount = 0;
    private float $totalSleepTime = 0;
    private float $startTime;

    public function __construct(
        private readonly int   $batchSize          = 100,
        private readonly float $delayBetweenBatches = 1.0,   // バッチ間の待機(秒)
        private readonly float $delayBetweenItems  = 0.01,   // アイテム間の待機(秒)
        private readonly int   $maxPerSecond        = 0       // 0=無制限
    ) {
        $this->startTime = microtime(true);
    }

    /**
     * アイテムの配列をバッチ処理する
     *
     * @param iterable $items     処理対象のアイテム
     * @param callable $processor 各アイテムの処理関数
     */
    public function process(iterable $items, callable $processor): array
    {
        $results    = [];
        $batch      = [];
        $batchCount = 0;

        foreach ($items as $item) {
            $batch[] = $item;

            if (count($batch) >= $this->batchSize) {
                $batchCount++;
                $results = array_merge($results, $this->processBatch($batch, $processor, $batchCount));
                $batch   = [];

                // バッチ間の待機
                if ($this->delayBetweenBatches > 0) {
                    echo "バッチ{$batchCount}完了: {$this->delayBetweenBatches}秒待機\n";
                    usleep((int) ($this->delayBetweenBatches * 1_000_000));
                    $this->totalSleepTime += $this->delayBetweenBatches;
                }
            }
        }

        // 残りのアイテムを処理
        if (!empty($batch)) {
            $batchCount++;
            $results = array_merge($results, $this->processBatch($batch, $processor, $batchCount));
        }

        return $results;
    }

    private function processBatch(array $batch, callable $processor, int $batchNo): array
    {
        $results        = [];
        $batchStartTime = microtime(true);

        foreach ($batch as $item) {
            $results[] = $processor($item);
            $this->processedCount++;

            // アイテム間の待機
            if ($this->delayBetweenItems > 0) {
                usleep((int) ($this->delayBetweenItems * 1_000_000));
                $this->totalSleepTime += $this->delayBetweenItems;
            }

            // レート制限チェック(maxPerSecond が設定されている場合)
            if ($this->maxPerSecond > 0) {
                $this->enforceRateLimit($batchStartTime, count($results));
            }
        }

        return $results;
    }

    /**
     * 処理速度が maxPerSecond を超えないよう調整する
     */
    private function enforceRateLimit(float $startTime, int $count): void
    {
        $elapsed  = microtime(true) - $startTime;
        $expected = $count / $this->maxPerSecond;

        if ($expected > $elapsed) {
            $waitMicro = (int) (($expected - $elapsed) * 1_000_000);
            usleep($waitMicro);
            $this->totalSleepTime += ($expected - $elapsed);
        }
    }

    public function getStats(): array
    {
        $elapsed = microtime(true) - $this->startTime;
        return [
            'processed'       => $this->processedCount,
            'elapsed_sec'     => round($elapsed, 2),
            'total_sleep_sec' => round($this->totalSleepTime, 2),
            'active_sec'      => round($elapsed - $this->totalSleepTime, 2),
            'avg_per_sec'     => $elapsed > 0
                ? round($this->processedCount / $elapsed, 1)
                : 0,
        ];
    }
}

$processor = new RateLimitedBatchProcessor(
    batchSize:           3,
    delayBetweenBatches: 0.1,
    delayBetweenItems:   0.0
);

$items   = range(1, 7);
$results = $processor->process($items, fn($n) => $n * 2);

echo "処理結果: " . implode(', ', $results) . "\n";
print_r($processor->getStats());

/*
出力例:
バッチ1完了: 0.1秒待機
バッチ2完了: 0.1秒待機
処理結果: 2, 4, 6, 8, 10, 12, 14
Array (
    [processed]       => 7
    [elapsed_sec]     => 0.22
    [total_sleep_sec] => 0.2
    [active_sec]      => 0.02
    [avg_per_sec]     => 31.8
)
*/

例4:スケジューラー・定期実行クラス

<?php
/**
 * sleep() を使ってCLIスクリプト内でタスクを定期実行するクラス
 * cron が使えない環境や細かいインターバル制御に有効
 */
class SimpleScheduler
{
    private bool  $running  = false;
    private array $tasks    = [];
    private array $lastRuns = [];

    /**
     * タスクを登録する
     *
     * @param string   $name     タスク名
     * @param int      $interval 実行間隔(秒)
     * @param callable $callback 実行する処理
     */
    public function addTask(string $name, int $interval, callable $callback): void
    {
        $this->tasks[$name]    = ['interval' => $interval, 'callback' => $callback];
        $this->lastRuns[$name] = 0;
        echo "タスク登録: {$name}({$interval}秒間隔)\n";
    }

    /**
     * スケジューラーを起動して定期実行ループを開始する
     *
     * @param int $runFor 実行する秒数(0 = 無限ループ)
     * @param int $tick   ループの最小間隔(秒)
     */
    public function run(int $runFor = 0, int $tick = 1): void
    {
        $this->running = true;
        $startTime     = time();

        echo "スケジューラー起動\n";

        while ($this->running) {
            $now = time();

            // 実行時間制限チェック
            if ($runFor > 0 && ($now - $startTime) >= $runFor) {
                echo "実行時間 {$runFor}秒 経過・終了\n";
                break;
            }

            // 各タスクの実行チェック
            foreach ($this->tasks as $name => $task) {
                if (($now - $this->lastRuns[$name]) >= $task['interval']) {
                    $this->runTask($name, $task['callback'], $now);
                }
            }

            sleep($tick);
        }
    }

    public function stop(): void
    {
        $this->running = false;
    }

    private function runTask(string $name, callable $callback, int $now): void
    {
        $this->lastRuns[$name] = $now;
        $start = microtime(true);

        try {
            $callback();
            $elapsed = round((microtime(true) - $start) * 1000, 1);
            echo "[" . date('H:i:s') . "] {$name} 実行完了({$elapsed}ms)\n";
        } catch (\Throwable $e) {
            echo "[" . date('H:i:s') . "] {$name} エラー: {$e->getMessage()}\n";
        }
    }
}

$scheduler = new SimpleScheduler();

$scheduler->addTask('キャッシュクリア', 5, function (): void {
    echo "  → キャッシュをクリアしました\n";
});

$scheduler->addTask('ハートビート', 2, function (): void {
    echo "  → ハートビート送信\n";
});

// 10秒間実行するデモ
// $scheduler->run(runFor: 10, tick: 1);

例5:ウォームアップ・スロットルクラス

<?php
/**
 * 処理速度を徐々に上げる「ウォームアップ」と
 * 速度上限を守る「スロットリング」を組み合わせたクラス
 * DB・API・キャッシュへの急激な負荷を防ぐ
 */
class ThrottledProcessor
{
    private int   $processedCount  = 0;
    private float $windowStartTime;
    private int   $windowCount     = 0;

    public function __construct(
        private readonly int   $maxPerWindow     = 10,   // ウィンドウあたりの最大処理数
        private readonly float $windowSeconds    = 1.0,  // ウィンドウサイズ(秒)
        private readonly int   $warmupCount      = 0,    // ウォームアップ件数
        private readonly float $warmupDelaySeconds = 0.5 // ウォームアップ中の待機時間
    ) {
        $this->windowStartTime = microtime(true);
    }

    /**
     * スロットル制御付きで1件処理する
     */
    public function process(callable $operation): mixed
    {
        $this->throttle();
        $result = $operation();
        $this->processedCount++;
        $this->windowCount++;
        return $result;
    }

    private function throttle(): void
    {
        // ウォームアップ期間中は追加ディレイ
        if ($this->warmupCount > 0 && $this->processedCount < $this->warmupCount) {
            $warmupProgress = $this->processedCount / $this->warmupCount;
            $delay = $this->warmupDelaySeconds * (1 - $warmupProgress);
            if ($delay > 0.001) {
                usleep((int) ($delay * 1_000_000));
            }
            return;
        }

        // スライディングウィンドウでレート制限
        $now     = microtime(true);
        $elapsed = $now - $this->windowStartTime;

        if ($elapsed >= $this->windowSeconds) {
            // ウィンドウをリセット
            $this->windowStartTime = $now;
            $this->windowCount     = 0;
            return;
        }

        if ($this->windowCount >= $this->maxPerWindow) {
            // ウィンドウが終わるまで待機
            $waitSec = $this->windowSeconds - $elapsed;
            echo "  スロットル: {$waitSec}秒待機\n";
            usleep((int) ($waitSec * 1_000_000));
            $this->windowStartTime = microtime(true);
            $this->windowCount     = 0;
        }
    }

    public function getProcessedCount(): int
    {
        return $this->processedCount;
    }
}

$throttle = new ThrottledProcessor(
    maxPerWindow:       5,
    windowSeconds:      1.0,
    warmupCount:        3,
    warmupDelaySeconds: 0.2
);

for ($i = 1; $i <= 8; $i++) {
    $throttle->process(function () use ($i): void {
        echo "処理 #{$i}\n";
    });
}
echo "合計: " . $throttle->getProcessedCount() . " 件処理\n";

例6:精密なタイミング制御クラス

<?php
/**
 * sleep() / usleep() / time_sleep_until() を使い分けて
 * 精密な時間制御を行うクラス
 * メトロノーム的な一定間隔実行・フレームレート制御などに使用する
 */
class PreciseTimer
{
    private float $lastTickTime;
    private int   $tickCount   = 0;
    private array $tickLog     = [];

    public function __construct(
        private readonly float $targetIntervalSeconds
    ) {
        $this->lastTickTime = microtime(true);
    }

    /**
     * 前回のtickからの経過時間が targetInterval になるまで待機する
     * 処理時間を考慮して正確な間隔を保つ
     */
    public function tick(): float
    {
        $this->tickCount++;
        $now      = microtime(true);
        $elapsed  = $now - $this->lastTickTime;
        $waitTime = $this->targetIntervalSeconds - $elapsed;

        if ($waitTime > 0) {
            // time_sleep_until で目標時刻まで精密に待機
            $targetTime = $this->lastTickTime + $this->targetIntervalSeconds;
            time_sleep_until($targetTime);
        }

        $actualInterval = microtime(true) - $this->lastTickTime;
        $this->lastTickTime = microtime(true);

        $this->tickLog[] = [
            'tick'     => $this->tickCount,
            'interval' => round($actualInterval * 1000, 2),
            'drift_ms' => round(($actualInterval - $this->targetIntervalSeconds) * 1000, 2),
        ];

        return $actualInterval;
    }

    /**
     * 指定回数のtickを実行してタイミング精度を計測する
     */
    public function benchmark(int $ticks, callable $work): array
    {
        $this->tickLog    = [];
        $this->tickCount  = 0;
        $this->lastTickTime = microtime(true);

        for ($i = 0; $i < $ticks; $i++) {
            $work($i + 1);
            $this->tick();
        }

        $intervals = array_column($this->tickLog, 'interval');
        $drifts    = array_column($this->tickLog, 'drift_ms');

        return [
            'ticks'          => $ticks,
            'target_ms'      => $this->targetIntervalSeconds * 1000,
            'avg_interval_ms'=> round(array_sum($intervals) / count($intervals), 2),
            'max_drift_ms'   => round(max(array_map('abs', $drifts)), 2),
            'avg_drift_ms'   => round(array_sum(array_map('abs', $drifts)) / count($drifts), 2),
        ];
    }

    public function getTickLog(): array
    {
        return $this->tickLog;
    }
}

// 0.1秒間隔で5回実行してタイミング精度を計測
$timer = new PreciseTimer(targetIntervalSeconds: 0.1);
$stats = $timer->benchmark(
    ticks: 5,
    work: fn(int $n) => null // 実際には何らかの処理
);

echo "タイミング精度レポート:\n";
foreach ($stats as $key => $val) {
    printf("  %-20s: %s\n", $key, $val);
}

/*
出力例:
タイミング精度レポート:
  ticks               : 5
  target_ms           : 100
  avg_interval_ms     : 100.12
  max_drift_ms        : 0.38
  avg_drift_ms        : 0.21
*/

sleep() を使う際の注意事項

Webリクエスト内での使用

<?php
// ⚠️ Webリクエスト内で sleep() を使うと、その間ユーザーは待ち続ける
// max_execution_time も消費し続ける

// ❌ NG:Webリクエストで長時間 sleep
sleep(30); // ユーザーが30秒待つ・タイムアウトリスク

// ✅ 長時間処理はキューに積んでCLIで非同期実行する
// $queue->push(['job' => 'send_email', 'user_id' => 42]);
// return response('メール送信を受け付けました');

set_time_limit() との関係

<?php
// sleep() で停止している時間は max_execution_time に加算されない
// (UNIX環境のみ。Windowsでは加算される)
set_time_limit(10);

sleep(5);  // この5秒は実行時間にカウントされない(UNIX)
// 実際の処理に10秒かけられる

シグナルによる中断

<?php
// sleep() はシグナルによって中断される場合がある
// 戻り値が 0 でなければ中断された(残り秒数が返る)
$remaining = sleep(10);
if ($remaining > 0) {
    echo "シグナルで中断: 残り{$remaining}秒\n";
}

関連関数との比較

関数精度戻り値特徴
sleep(int $sec)残り秒数 or 0最もシンプル
usleep(int $micro)マイクロ秒voidミリ秒制御に最適
time_nanosleep(int, int)ナノ秒true or array最高精度、残り時間も取得可
time_sleep_until(float)マイクロ秒bool指定時刻まで待機

よくある落とし穴

<?php
// ❌ NG:PHP 8.0以降、負の値は ValueError
sleep(-1); // ValueError: sleep(): Argument #1 ($seconds) must be greater than or equal to 0

// ✅ 0以上であることを確認する
$seconds = max(0, $calculatedDelay);
sleep($seconds);
<?php
// ❌ よくある誤解:sleep() が max_execution_time を消費すると思っている
// UNIX 環境では sleep 中の時間は実行時間にカウントされない
ini_set('max_execution_time', 5);
sleep(10); // UNIX では問題なし(Windows では Fatal error になる)
echo "完了\n";

// ✅ ポータブルなコードでは注意が必要
<?php
// ❌ 精度が必要なのに sleep() を使っている
sleep(1); // 最小単位が1秒なので細かい制御ができない

// ✅ ミリ秒制御には usleep() を使う
usleep(500_000); // 500ms
usleep(100_000); // 100ms
usleep(50_000);  //  50ms

まとめ

項目内容
関数名sleep(int $seconds): int
主な用途スクリプト実行を秒単位で一時停止する
戻り値正常終了: 0 / シグナル中断: 残り秒数
精度秒単位(細かい制御には usleep() を使う)
ミリ秒待機usleep(N * 1_000)
指定時刻まで待機time_sleep_until(microtime(true) + $seconds)
max_execution_timeUNIX環境では sleep 中は加算されない
Webリクエストでの使用原則避ける(長時間処理はキュー化を推奨)

sleep() はシンプルながら、リトライの間隔制御・ポーリング・バッチ処理の負荷分散・定期実行など実践的な場面で欠かせない関数です。秒精度で十分な場面では sleep()、ミリ秒・マイクロ秒が必要なら usleep()、特定時刻まで待ちたいなら time_sleep_until() と使い分けることで、精度と可読性を両立できます。


参考リンク

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