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 придётся пересоздавать с нуля. Но зато это откроет много возможностей в мире информатики, уже охваченном многопоточностью.
chtulhu
Не совсем понял где тут полноценность, если файбер принимающий подключения работает эксклюзивно, а другие файберы ждут пока он засаспендит себя. Я верно понял?
По моему это обертка над корутинами и генераторами, которые были добавлены еще в php 5.5. Что я могу сделать с помощью файберов такого, что не смогу сделать с корутинами и генераторами?
Tatikoma
Идея в том, что все IO операции вы выполняете асинхронно. Таким образом если в вашем приложении весь код асинхронный, то запуск одного процесса позволит обрабатывать сколько угодно подключений, пока хватает одного ядра процессора.
Если не хватает одно ядра, то опять же не беда — можете использовать балансировщик, или прямо опцию SO_REUSEPORT, чтобы расширить один сокет на несколько процессов.
Файберы не дают принципиально нового функционала, всё это уже сейчас можно делать в amphp, reactphp, swoole (включая http-сервер, кстати). И это действительно круто работает (у меня в проде, по крайней мере). Файберы дают возможность писать чистый код, значительно более чистый, который без них или аналога принципиально невозможен в PHP.
chtulhu
Можете поделиться мини-примером такого принципиально невозможного кода(обычный php+fiber). Пример с этой страницы без проблем переделывается под корутину и генератор.
NiceDay
ну как минимум файберы дадут возможность нормально указывать возвращаемый тип, а не Generator / Amp\Promise, в который заворачивается результат.
кроме того, вероятно, появится возможность более изящно использовать асинхроный код в синхронной среде, чем сейчас — Amp\Promise\wait(Amp\call(fn () => yield $coroutine));
Tatikoma
Как например: await в виде функции, а не через обёртку с колбеком с yield'ом (как это сейчас в amphp).