PHP пытается восполнить недостаток возможностей в своей кодовой базе, и Fiber’ы — одно из значимых нововведений. Они появились в PHP 8.1 в конце 2020 и привнесли в язык своего рода асинхронное программирование. Файберы представляют собой легковесные потоки исполнения (известные как сопрограммы, или корутины (coroutine)). Они исполняются параллельно, но обрабатываются исключительно самой runtime-средой, а передаются напрямую в процессор. Разные реализации сопрограмм есть во многих основных языках, но принцип один и тот же: позволить компьютеру одновременно выполнять две и больше задач и ждать, пока они все не завершатся.

PHP-реализация файберов — это не настоящие асинхронные вычисления, как можно подумать. Даже после появления новинки ядро языка осталось синхронным. Применение файберов можно сравнить с пересаживанием из одного автомобиля в другой.

Как работают файберы?


Файбер — это одиночный финальный класс, который похож на автомобиль: немедленно заводится и работает, жмёт на тормоза и ждёт, возобновляет работу.

final class Fiber
{
    public function __construct(callable $callback) {}
    public function start(mixed ...$args): mixed {}
    public function resume(mixed $value = null): mixed {}
    public function throw(Throwable $exception): mixed {}
    public function isStarted(): bool {}
    public function isSuspended(): bool {}
    public function isRunning(): bool {}
    public function isTerminated(): bool {}
    public function getReturn(): mixed {}
    public static function this(): ?self {}
    public static function suspend(mixed $value = null): mixed {}
}

Если создать новый экземпляр файбера с вызываемым объектом, то ничего не произойдёт. Пока вы не запустите файбер, callback будет выполняться так же, как любой другой нормальный PHP-код.

$fiber = new Fiber(function() : void {
  echo "I'm running a Fiber, yay!";
});$fiber->start(); // I'm running a Fiber, yay!

Я не упоминал, что файберы асинхронные? Они такие, но только до тех пор, пока вы не нажмёте на тормоза с помощью вызова Fiber::suspend() внутри callback’а. После этого файбер передаёт управление «наружу», но помните, что эта файбер-машина ещё жива и ждёт возвращения к работе.

$fiber = new Fiber(function() : void {
  Fiber::suspend();
  echo "I'm running a Fiber, yay!";
});$fiber->start(); // [Nothing happens]

Пока файбер стоит на паузе, нужно убрать ногу с тормоза — вызвать извне метод resume().

$fiber = new Fiber(function() : void {
   Fiber::suspend();
   echo "I'm running a Fiber, yay!";
});
$fiber->start(); // [Nothing happened]
$fiber->resume(); // I'm running a Fiber, yay!

Это фальшивая асинхронность, но это не означает, что ваше приложение не может выполнять две задачи одновременно. На самом деле система сохраняет текущее состояние функции Fiber. Вы буквально пересаживаетесь между разными автомобили, ведя их в одно и то же место.

Есть нюансы в том, как методы start(), suspend() и resume() принимают аргументы:

  • Метод start() передаёт аргументы вызываемому объекту и возвращает всё, что принимает метод suspend().
  • Метод suspend() возвращает любое значение, которое принял метод resume().
  • Метод resume() возвращает всё, что принято при следующем вызове suspend().

Это относительно упрощает взаимодействие между основным потоком и файбером:

  • resume() используется для помещения в файбер значений, принятых suspend(),
  • а suspend() используется для отправки значений, принятых resume().

Так будет гораздо проще понять пример из официальной документации:

$fiber = new Fiber(function (): void {
    $value = Fiber::suspend('fiber');
    echo "Value used to resume fiber: ", $value, "\n";
});
$value = $fiber->start();
echo "Value from fiber suspending: ", $value, "\n";
$fiber->resume('test');

Если выполнить этот код, то вы получите подобное:

Value from fiber suspending: fiber
Value used to resume fiber: test

Скоро у нас будет свой полноценный веб-сервер


Посмотрим правде в глаза: в 99 % случаев PHP используется вместе с nginx/Apache, в основном из-за того, что этот язык не многопоточный. Сервер в PHP блокирующий, он используется только для каких-нибудь тестов или отображения информации на клиенте. Файберы могут помочь PHP эффективнее работать с сокетами, и тогда у нас будет что-нибудь наподобие WebSockets, серверных событий, групповых подключений к базе данных, даже HTTP/3, и всё это без необходимости компилировать расширения, писать хаки с помощью непредназначенных для этого функций, инкапсулировать PHP во внешнюю runtime-среду или прибегать к другим ухищрениям, создающим проблемы.

Иногда нужно время, чтобы что-то получить, но если нам пообещают сохранить для других функций ту же кодовую базу без необходимости тратить дни на попытки компилирования и развёртывания, то я готов ждать.

Вы не будете пользоваться файберами напрямую


Согласно документации, файберы предлагают «лишь самый минимум, необходимый для реализации в пользовательском PHP-коде полностековых сопрограмм или «зелёных» потоков». Иными словами, если у вас не появятся очень странные причины, чтобы использовать их напрямую, вы никогда не будете взаимодействовать с файберами так же, как «корутинами» в JavaScript и Go.

Авторы некоторых высокоуровневых фреймворков (вроде Symfony, Laravel, CodeIgniter, CakePHP и прочих) возьмут паузу, чтобы выбрать подход к файберам и создать набор инструментов для работы с ними с точки зрения разработчика. А некоторые низкоуровневые фреймворки наподобие amphp и ReactPHP уже предлагают файберы в своих свежайших версиях.


Хотя это освободит вас от необходимости думать о файберах, а не о своих идеях, однако теперь нас ждёт всплеск конкуренции со всеми её достоинствами и недостатками.

По одному файберу за раз



Процитирую Аарона Пиотровски из PHP Internals Podcast #74:

Поскольку одновременно может выполняться только один файбер, у вас не возникнет чего-то вроде состояния гонки, когда к памяти обращается сразу два потока.

Аарон также добавил, что фреймворки смогут решить проблему распараллеливания и синхронизации при работе с одним фрагментом памяти. Это хорошо, потому что вам не придётся думать о состоянии гонки, семафорах и мьютексах, о которых прекрасно знают гоферы. Но что бы вы ни делали, у вас по-прежнему будет одновременно выполняться только что-то одно.

При этом никаких каналов


Поскольку одновременно может выполняться только один файбер, то даже если вы объявите несколько штук, у вас не будет проблем с синхронизацией данных. Однако Аарон сказал, что может проснуться другой файбер и переписать данные, которые расшарил первый файбер. Одним из решений в такой ситуации будет использование чего-то вроде каналов в Go. По словам Аарона, кроме файберов придётся реализовать что-то ещё, а до тех пор способ использования каналов будет зависеть от фреймворков, если их авторам каналы покажутся необходимыми, как в случае с amphp.

Новичок на районе


В последние месяцы популярность Go росла, в основном благодаря тому, что он изначально создавался с учётом распараллеливания. С помощью ключевого слова go в нём всё можно исполнять параллельно, а за синхронизацию отвечают мьютексы или каналы, которые облегчают нам работу.

names := make(chan string)go doFoo(names)
go doBar(names)

С этой точки зрения Go далеко впереди PHP с его зачаточным распараллеливанием. Если вам нужна настоящая многопоточность, то пишите на Go, или даже на Rust, если вам нужно напрямую использовать процессорные потоки.

Это не означает, что PHP не может конкурировать с какими-либо моделями распараллеливания, но в своей основе это ещё синхронный язык в угоду удобству и простоте понимания. К примеру, Go страдает от чрезмерного plumbing. Если нужна настоящая модель распараллеливания, как в Go, то PHP придётся пересоздавать с нуля. Но зато это откроет много возможностей в мире информатики, уже охваченном многопоточностью.