Создание корутин
function counter(string $name): void {
for ($i = 1; $i <= 5; $i++) {
echo "$name: $i\n";
sleep(1);
}
}
counter('A');Функция counter выводит на экран счётчик с паузой одна секунда:
A: 1
A: 2
A: 3
A: 4
A: 5Каждый раз, когда вызывается sleep, поток PHP засыпает на указанное время. В этот момент он буквально ничего не делает. Скрипт PHP работает 5 секунд, ровно столько, сколько ждёт сама функция.
Что будет, если запустить функцию counter в корутине?
use function Async\spawn;
spawn(counter(...), 'B');
counter('A');На экране появится чередование:
A: 1
B: 1
A: 2
B: 2
A: 3
B: 3
A: 4
B: 4
A: 5
B: 5Общее время работы скрипта по-прежнему составляет около 5 секунд, однако скрипт теперь выполняет как бы две функции "одновременно". Что же происходит?
spawn создаёт второй логический поток управления "B", внутри которого выполняется функция counter. Когда в потоке управления "A" встречается sleep, вместо того чтобы заблокировать весь PHP полностью, он передаёт управление в другой логический поток "B". И так далее:
A sleep -> B
B sleep -> A
A sleep -> B
...В чём выгода?
Представьте, что функция counter — привычный последовательный код, который совершает операции ввода-вывода, работу с таймером (sleep). Операции ввода-вывода выполняются в ядре операционной системы, поэтому код PHP обязан их ждать. В это же самое время PHP мог бы делать что-то ещё. Корутины позволяют заполнить ожидание полезной работой без создания отдельных процессов, потоков, синхронизации, гонки данных и многих ужасов параллельного программирования.
Рассмотрим практичный пример:
function processUsers(string $path, &$counter): void
{
$handle = fopen($path, 'r');
$header = fgetcsv($handle);
$loginIndex = array_search('login', $header);
$emailIndex = array_search('email', $header);
while (($row = fgetcsv($handle)) !== false) {
$login = $row[$loginIndex];
$email = $row[$emailIndex];
$counter++;
}
fclose($handle);
}Функция processUsers читает CSV-файл и обрабатывает каждую строку. Файл большой, выводить каждую строчку на экран не нужно, но хотелось бы видеть прогресс. Можно рисовать прогресс на каждую итерацию, но из-за этого упадёт производительность обработки. Можно рисовать прогресс на каждую 100-итерацию, но строки могут обрабатываться за разное время. Как вывести прогресс плавно с примерно одинаковым интервалом?
use function Async\spawn;
use function Async\delay;
function printProgress(int $current, int $total, int $width = 30): void
{
$ratio = $total > 0 ? $current / $total : 1;
$filled = (int) round($ratio * $width);
$bar = str_repeat('=', $filled) . str_repeat(' ', $width - $filled);
echo "\r[$bar] " . round($ratio * 100) . "%";
}
$counter = 0;
$total = 100_000;
$progress = spawn(function() use (&$counter, $total) {
while (true) {
printProgress($counter, $total);
delay(1000);
}
});
processUsers('users.csv', $counter);
$progress->cancel();Этого можно добиться с помощью корутины $progress, которая отображает текущий прогресс с частотой 1 раз в секунду. Для этого используется переменная $counter, которая увеличивается в функции processUsers, и передаётся в корутину через ссылку.
[====> ] 15%Красота решения состоит в том, что printProgress ничего не знает про processUsers, и наоборот. Они не зависят друг от друга, но при этом работают вместе.
То есть корутины помогают не только создать иллюзию одновременного выполнения, но и помогают разделить ответственность.
А вы заметили $progress->cancel()? Зачем интересно он нужен?