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

Створення корутин

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, яка раз на секунду відображає поточний прогрес. Вона покладається на змінну $counter, яка збільшується всередині функції processUsers і передається в корутину за посиланням.

bash
[====>                         ] 15%

Краса цього рішення в тому, що printProgress нічого не знає про processUsers, і навпаки. Вони не залежать одна від одної, проте працюють разом.

Іншими словами, корутини не просто створюють ілюзію конкурентного виконання, вони також допомагають розділити відповідальність.

Ви помітили $progress->cancel()? Для чого саме він тут?