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