Отслеживание прогресса выполнения задачи на PHP

С помощью тиков Вы можете сделать вывод статуса прогресса паралельно выполнению основной задаче.
Делается это с помощью следующих функций: register_tick_function и unregister_tick_function. А так же задаётся интервал вызова этих функций в тиках с помощью declare(ticks=N), где N - количество тиков.
Один тик это слишком маленький интервал для того что бы выводить сообщение с прогрессом каждый раз, а тем более сохранять его куда то что бы показать клиенту.
По этому я предлагаю использовать для вывода дополнительные переменные которые будут выполнять нужные операции с определённой задержкой.
Вот простой пример:

<?php
// Переменная для эмуляции основной работы
$a = 1;

// Время последнего вывода статуса
$lastUpdate = time();

/*
 * Для уменьшения нагрузки запускаем функцию каждые 10 000 тиков
 */
declare(ticks=10000);

/**
 * Функция для вывода прогресса.
 * Будет выводить данные каждую секунду.
 */
function outProgress()
{
    global $a, $lastUpdate;
    if ($lastUpdate != time()) {
        $lastUpdate = time();
        echo ceil($a / 1000) . "%\n";
    }
}

// Регистрируем функцию для вывода прогресса
register_tick_function('outProgress');

// Эмулируем сложную задачу
while ($a < 100000) {
    $a++;
    usleep(100);
}

Для более практических целей нам понадобиться класс который будет предоставлять нам обёртку для этих функций, и будет более простой в использование.

<?php

class Timer {
    
    private $_lastRun;
    
    private $_interval;
    
    private $_function;
    
    private $_params = array();
    
    private $_hasRun = false;
            

    function __construct($interval, $function, $arg) {
        $this->_interval = $interval;
        $this->_function = $function;
        if ($arg != null) {
            $this->_params = array_slice(func_get_args(), 2);
        }
    }
    
    public function Start() {
        if ($this->_hasRun) {
            return;
        }
        $this->_lastRun = time();
        register_tick_function(array($this, 'Tick'));
        $this->_hasRun = true;
    }
    
    public function Stop() {
        if (!$this->_hasRun) {
            return;
        }
        unregister_tick_function(array($this, 'Tick'));
        $this->_hasRun = false;
    }
    
    public function Tick() {
        if ($this->_lastRun + $this->_interval < time()) {
            return;
        }
        
        call_user_func_array($this->_function, $this->_params);
        
        $this->_lastRun = time();
    }
}

А теперь используем его:

<?php

include './Timer.php';

// Переменная для эмуляции основной работы
$a = 1;

/**
 * Функция для вывода прогресса.
 * Будет выводить данные каждую секунду.
 */
function outProgress($param)
{
    global $a;
    echo $param . " : " . ceil($a / 1000) . "%\n";
}

$timer = new Timer(1, 'outProgress', 'test');
$timer->Start();

declare(ticks=10000) {
    // Эмулируем сложную задачу
    while ($a < 100000) {
        $a++;
        usleep(100);
        if ($a > 50000) {
            $timer->Stop();
        }
    }
}

Как видим, теперь у нас появилось больше возможностей: мы можем запускать и останавливать для нужных нам задач, а так же у нас есть удобный способ указания интервала. Так же у нас сохраняется возможность передачи параметров в функцию таймера.

Замечание 1

Мы не можем останавливать работу таймера при срабатывание самого таймера.

function outProgress($param)
{
    global $a;
    echo $param . " : " . ceil($a / 1000) . "%\n";
    if ($a > 50000) {
        $timer->Stop(); //Выдаст предупреждение и таймер НЕ ОСТАНОВИТЬСЯ.
    }
}

Замечание 2
declare(ticks=N) необходимо объявлять в том месте где у вас будет выполняться код, ход выполнения которого вы хотите отслеживать. Если объявить его в классе Timer то результата не будет.

Автор: Сергей Степанов

Поделиться @
aaa, 28 июня 2022 в 21:11