ОсновыКорутины

Создание корутин

php
function counter(string $name): void {
    for ($i = 1; $i <= 5; $i++) {
        echo "$name: $i\n";
        sleep(1);
    }
}

counter('A');

Функция counter выводит на экран счётчик с паузой одна секунда:

bash
A: 1
A: 2
A: 3
A: 4
A: 5

Каждый раз, когда вызывается sleep, поток PHP засыпает на указанное время. В этот момент он буквально ничего не делает. Скрипт PHP работает 5 секунд, ровно столько, сколько ждёт сама функция.

Что будет, если запустить функцию counter в корутине?

php
use function Async\spawn;

spawn(counter(...), 'B');
counter('A');

На экране появится чередование:

bash
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". И так далее:

text
A sleep -> B
B sleep -> A
A sleep -> B
...

В чём выгода?

Представьте, что функция counter — привычный последовательный код, который совершает операции ввода-вывода, работу с таймером (sleep). Операции ввода-вывода выполняются в ядре операционной системы, поэтому код PHP обязан их ждать. В это же самое время 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-итерацию, но строки могут обрабатываться за разное время. Как вывести прогресс плавно с примерно одинаковым интервалом?

php
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, и передаётся в корутину через ссылку.

bash
[====>                         ] 15%

Красота решения состоит в том, что printProgress ничего не знает про processUsers, и наоборот. Они не зависят друг от друга, но при этом работают вместе.

То есть корутины помогают не только создать иллюзию одновременного выполнения, но и помогают разделить ответственность.

А вы заметили $progress->cancel()? Зачем интересно он нужен?