<? phpukraine СПІВБЕСІДИ
Пошук по платформі
CORE PHP · MIDDLE

Що таке Fibers і навіщо вони в PHP 8.1+?

`Fiber` (PHP 8.1+) — це низькорівневий примітив кооперативної багатозадачності: блок коду з власним стеком, який можна призупинити з будь-якої глибини викликів через `Fiber::suspend()` і відновити через `resume()`. Планувальника й циклу подій у ядрі немає — їх дають revolt/event-loop та AMPHP v3.

Fibers — це нарешті багатопоточність у PHP?
Чим `Fiber` відрізняється від генератора, якщо обидва вміють призупинятися й віддавати значення?
Я обгорнув запити через PDO у `Fiber`, а сторінка не пришвидшилась — чому?
Ви колись писали `new Fiber()` руками? Якщо ні, то навіщо воно взагалі в мові?
Fiber PHP 8.1 корутини concurrency amphp revolt event loop

Fiber — це клас у глобальному просторі імен, доданий у PHP 8.1 разом із FiberError. Обʼєкт створюють від будь-якого callable (new Fiber($callback)), запускають через start(...$args), а всередині коду волокна викликають статичний Fiber::suspend($value) — виконання завмирає, а start() повертає передане значення тому, хто волокно запустив. Далі resume($value) продовжує роботу з тієї ж точки, причому аргумент resume() стає значенням, яке поверне Fiber::suspend(). Стан читають через isStarted(), isSuspended(), isRunning(), isTerminated(), результат callable — через getReturn() (до завершення волокна це FiberError), а «розбудити з винятком» можна через throw(). Усе разом це один примітив: перемикання стеків, і нічого більше.

Порівняння з генераторами — головна змістовна частина відповіді. Обидва механізми кооперативні й обидва вміють двобічний обмін (Generator::send() проти Fiber::resume()), але генератор призупиняє тільки власне тіло: yield має стояти в тій самій функції, а функція з yield перестає бути звичайною — вона повертає Generator, і викликач мусить її ітерувати. Щоб призупинитися з глибини, кожну функцію в ланцюжку доводиться робити генератором і прокидати yield from — це та сама «розфарбованість функцій», через яку синхронний і корутинний код не змішувалися. Волокно має власний стек, тому Fiber::suspend() спрацьовує на будь-якій глибині, а проміжний код лишається звичайним і взагалі не знає про своє оточення; за потреби бібліотека перевіряє контекст через Fiber::getCurrent(). Зворотний бік: волокно не є Iterator, у нього немає ключів і foreach, тож для лінивого перебору даних генератор нікуди не подівся.

Руками new Fiber() майже ніхто не пише, і це нормальна відповідь на питання «навіщо воно тоді». RFC свідомо лишив у ядрі лише примітив, без планувальника й циклу подій, бо його місце — у користувацькому просторі. Планувальником став revolt/event-loop — спільний цикл подій, який використовують AMPHP v3 і ReactPHP; поверх нього amphp/amp v3 дає Amp\async() (запускає волокно) та Future::await() (усередині кличе suspend()). Найкраща ілюстрація зиску — сама історія AMPHP: у v2 корутини будувалися на генераторах і кожен асинхронний виклик писався як yield $promise, у v3 бібліотеку переписали на волокна, і yield із користувацького коду зник — асинхронний виклик виглядає як звичайний. У ReactPHP той самий підхід дає пакет react/async зі своїми async()/await().

Чому це не async у розумінні JavaScript. По-перше, у PHP немає вбудованого рантайму з циклом подій: у Node цикл є завжди й усі I/O-API неблокуючі за замовчуванням, у PHP цикл треба принести бібліотекою і явно запустити. По-друге, у ядрі немає ані Promise, ані ключових слів async/await — є один клас Fiber. По-третє й найважливіше практично: волокно не перетворює блокуючий виклик на неблокуючий. PDO::query(), file_get_contents(), curl_exec(), sleep() зупиняють увесь процес разом з усіма волокнами, тож конкурентність зʼявляється лише тоді, коли ви користуєтесь неблокуючими клієнтами (amphp/http-client, amphp/mysql, amphp/postgres, amphp/socket). Це відрізняє підхід ядра PHP від Swoole, який підміняє блокуючі функції власними реалізаціями через runtime hooks.

Межі варто назвати одразу, щоб відповідь звучала як досвід, а не як переказ документації. Волокна — це конкурентність, а не паралелізм: у кожен момент виконується рівно одне волокно, і задачу, що впирається в CPU, вони не пришвидшать — там потрібні amphp/parallel, ext-parallel або кілька воркерів. У класичній моделі PHP-FPM виграш обмежений одним запитом: розпаралелити три звернення до зовнішніх API можна, а тримати цикл подій між запитами — ні (для цього потрібен довгоживучий воркер). Памʼять теж не безкоштовна: кожне призупинене волокно тримає власний стек, тому «мільйон волокон» — не той дизайн, який варто пропонувати. І нарешті, ресурси всередині волокна закривають у finally: коли призупинене волокно втрачає останнє посилання, PHP розкручує його стек саме заради finally, а призупинитися ще раз у цей момент уже не дасть.

// Функції нижче — звичайні: ні yield, ні Generator у сигнатурах
function fetchBody(string $url): string
{
    return readResponse($url);        // виклик на рівень глибше
}

function readResponse(string $url): string
{
    // тут був би неблокуючий сокет; призупиняємось із глибини стека
    $answer = Fiber::suspend("чекаю на {$url}");

    return "тіло {$url} ({$answer})";  // suspend() повертає те, що дали в resume()
}

$fibers = [
    new Fiber(fn (): string => fetchBody('/api/jobs')),
    new Fiber(fn (): string => fetchBody('/api/companies')),
];

// start() повертає значення, передане у Fiber::suspend()
foreach ($fibers as $fiber) {
    echo $fiber->start(), PHP_EOL;    // «чекаю на /api/jobs», «чекаю на /api/companies»
}

// Примітивний планувальник: обидва волокна вже «в польоті» одночасно
foreach ($fibers as $fiber) {
    if ($fiber->isSuspended()) {
        $fiber->resume('200 OK');     // значення повертається з suspend()
    }
}

foreach ($fibers as $fiber) {
    // getReturn() дійсний лише після завершення волокна
    echo $fiber->isTerminated() ? $fiber->getReturn() : 'ще виконується', PHP_EOL;
}

try {
    Fiber::suspend('з {main}');       // поза волокном це заборонено
} catch (FiberError $e) {
    echo $e->getMessage(), PHP_EOL;   // Cannot suspend outside of fiber
}
Що `Fiber` — це не «async у PHP», а лише механізм призупинення: RFC свідомо не додав ані планувальника, ані циклу подій, ані `Promise` в ядро.
Що головна відмінність від генератора — власний стек: `Fiber::suspend()` викликається з будь-якої глибини вкладених функцій, і проміжні функції не треба переписувати на `yield from`.
Що волокна кооперативні й однопотокові: у кожен момент виконується рівно одне, паралельності на кількох ядрах вони не дають.
Що реальні споживачі — `revolt/event-loop` (спільний цикл подій для AMPHP v3 і ReactPHP) та `amphp/amp` v3; AMPHP v2 будував корутини на генераторах і `yield`, v3 переписали на волокна й `yield` з користувацького коду зник.
Що блокуючий виклик (`PDO::query`, `file_get_contents`, `sleep`, `curl_exec`) усередині волокна блокує весь процес — волокно саме собою нічого не робить неблокуючим.
Казати «Fibers — це потоки» або «тепер PHP паралелить запити на ядрах»: це один процес, один потік, кооперативне перемикання за явним `suspend()`.
Обгортати у волокно звичайний `PDO`/`file_get_contents` і чекати прискорення: потрібні неблокуючі клієнти (`amphp/http-client`, `amphp/mysql`, `amphp/postgres`), інакше виграшу нуль.
Викликати `Fiber::suspend()` із `{main}` або з коду поза волокном — буде `FiberError: Cannot suspend outside of fiber`.
Читати `$fiber->getReturn()`, поки волокно ще призупинене — `FiberError`; спочатку `isTerminated()`.
Плутати ролі: брати `Fiber` там, де потрібна лінива послідовність даних — для ітерації по мільйону рядків правильний інструмент і далі генератор.
Стверджувати, що в PHP 8.1 зʼявилися `async`/`await` і `Promise`: у ядрі є лише клас `Fiber`, решта — бібліотеки.
ПОРАДА

Формула, яка закриває питання: «генератор — це корутина, видима в сигнатурі; волокно — корутина, невидима для викликаного коду». Далі одне речення про межі: «PHP отримав перемикач стеків, а планувальник і неблокуючий I/O довелося взяти з Revolt і AMPHP».

оновлено 3 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Саме власний стек відрізняє `Fiber` від генератора: `Fiber::suspend()` працює з будь-якої вкладеності, тоді як генератор призупиняє лише власне тіло й вимагає `yield from` через увесь ланцюжок. Паралельності волокна не дають (один потік), блокуючі API неблокуючими не стають, і до генераторів вони жодного стосунку не мають.