PHP 8.1: Fibers (Файберы)
Итак голосование прошло и RFC по Fibers был принят, хоть против проголосовала приличная часть сообщества.
Пытаясь восполнить пробел в своей кодовой базы, PHP выпускает одно из значимых дополнений к языку - Fibers. PHP Fibers, которые появятся в PHP 8.1 в конце года, представят для программистов функционал, своего рода асинхронного программирования (корутины), обеспечив легкий и управляемый параллелизм.
Концепция файберов в основном относится к легкому потоку выполнения (корутины/сопрограммы ). Кажется, что они выполняются параллельно, но в конечном итоге обрабатываются самой средой выполнения, а не отправляются напрямую в ЦП. У многих основных языков есть свои способы их реализации, но принцип тот же: пусть компьютер выполняет две или более задачи одновременно, и дождитесь, пока все не будет завершено.
Реализация Fibers в PHP не является истинным асинхронным вычислением, как можно подумать. В действительности ядро PHP по-прежнему останется синхронным после того как будут введены Fibers rfc.
Вы можете думать о PHP Fibers как о переключении с одной машины на другую.
Как работают файберы?
Файберы похожи на нити в компьютерной программе. Потоки планируются операционной системой и не гарантирует, когда и в какой момент потоки будут приостановлены и возобновлены. Файберы создаются, запускаются, приостанавливаются и завершаются самой программой и позволяют точно контролировать выполнение основной программы и выполнение файберов.
В свое время в PHP 5.4 были добавлены генераторы. С помощью генераторов можно было вернуть (yield) экземпляр генератора вызывающей стороне без удаления состояния блока кода. Генераторы не позволяли легко возобновить вызов с того места, где был вызван yield.
С помощью Fibers код внутри Fiber может приостановиться и вернуть любые данные в основную программу. Основная программа может возобновить работу Fiber с того места, где она была приостановлена .
Важно понимать, что многопоточное выполнение не означает одновременное выполнение . Файберы и основной поток не выполняются в одно и тоже время. Основной поток запускает файбер, и когда он запускается, то файбер уже выполняется исключительно сам. Основной поток не может наблюдать, завершать или приостанавливать файбер, пока файбер выполняется.
Файбер может приостановить самого себя, но не может возобновить свою работу - возобновить работу Fiber должен основной поток.
Файбер сам по себе не позволяет одновременно выполнять несколько файберов, а так же основной поток и файбер.
Fiber - это единственный финальный класс, что предотвращает его расширение другим пользовательским классом.
Файбер условно можно представить как автомобиль: он может завестись, и сразу же поехать, зажимать тормоз, ждать и возобновлять поездку.
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 {}
}
Когда вы создадите новый экземпляр Fiber, ничего не произойдет. Коллбек не будет выполняться, до тех пор, пока вы не запустите Fiber.
$fiber = new Fiber(function() : void {
echo "Поехали!";
});
$fiber->start(); // Поехали!
Помните, что файберы асинхронны? Можно сделать так, чтобы они как бы были, но оставались недвижимы, т.к. был "зажат тормоз" - Fiber::suspend(). Далее файбер передаст управление «наружу», но надо иметь в виду, что наш Fiber-автомобиль все еще жив и ожидает возобновление движения.
$fiber = new Fiber(function() : void {
Fiber::suspend();
echo "Поехали!";
});
$fiber->start(); // ничего не произойдет
Fiber::suspend() может быть вызван только внутри волокна.
Теперь, когда автомобиль стоит, следующее, что нужно сделать, - это снять ногу с тормоза, и для этого мы можем вызвать извне метод resume().
$fiber = new Fiber(function() : void {
Fiber::suspend();
echo "Поехали!";
});
$fiber->start(); // ничего не произойдет
$fiber->resume(); // Поехали!
В буквальном смысле это не асинхронность, но это не значит, что ваше приложение не может делать две вещи одновременно. Состояние функции Fiber сохраняется там, где оно было остановлено. Образно говоря, вы переключаетесь между автомобилями, ведя каждый к одной точке.
start(), suspend() и в resume() могут принимать аргументы:
- Метод start() будет передавать аргументы в коллбэк и вернет значение, независимо от метода suspend().
- Методе suspend() возвращает значение, полученное от метода resume() .
- Метод resume() возвращает то, что было получено после вызова suspend().
Это делает связь между основным потоком и Fiber относительно простой:
- resume() используется для "вталкивания" значений в Файбер, которые можно получить из suspend()
- suspend() используется для "выталкивания" значений из Файбера, полученных пользователем в том месте, где используется resume().
$fiber = new Fiber(function (): void {
$push = Fiber::suspend('вытолкнули');
echo "Значение для resume: ", $push, "\n";
});
$put = $fiber->start();
echo "Значение для suspend: ", $put, "\n";
$fiber->resume('втолкнули');
Значение для suspend: вытолкнули
Значение для resume: втолкнули
Резюме состояний файберов
- Запущенные файберы включают приостановленные, работающие и завершенные.
- Приостановленные файберы считаются запущенными, но не работающими или завершенными.
- Работающие файберы запускаются, но не завершаются и не приостанавливаются.
- Завершенные файберы запускаются, но не работают и не приостанавливаются.
Исключения для Fibers
Fiber в PHP 8.1 добавляет два новых класса Throwable. Ни один из них не может быть создан с помощью пользовательского кода PHP, потому что их выполнение ограничено в их конструкторе.
/**
* Exception thrown due to invalid fiber actions, such as resuming a terminated fiber.
*/
final class FiberError extends Error
{
/**
* Constructor throws to prevent user code from throwing FiberError.
*/
public function __construct()
{
throw new \Error('The "FiberError" class is reserved for internal use and cannot be manually instantiated');
}
}
/**
* Exception thrown when destroying a fiber. This exception cannot be caught by user code.
*/
final class FiberExit extends Exception
{
/**
* Constructor throws to prevent user code from throwing FiberExit.
*/
public function __construct()
{
throw new \Error('The "FiberExit" class is reserved for internal use and cannot be manually instantiated');
}
}
Примеры использования
Обратите внимание, что файберы, добавленные в PHP 8.1, хоть и предназначены для параллелизма, но не позволяют выполнять параллельную обработку в прямом понимании этого значения. Например, это не позволит запустить две загрузки файла Curl'ом одновременно. Файберы могут помочь в качестве базовых структур для цикла обработки событий параллельной обработки, чтобы легко управлять состоянием программы.
Ниже приводится простое приложение, показывающее последовательность выполнения
$fiber = new Fiber(function(): void {
echo "Hello from the Fiber...\n";
Fiber::suspend();
echo "Hello again from the Fiber...\n";
});
echo "Starting the program...\n";
$fiber->start();
echo "Taken control back...\n";
echo "Resuming Fiber...\n";
$fiber->resume();
echo "Program exits...\n";
результатом ее выполнения будет:
Starting the program...
Hello from the Fiber...
Taken control back...
Resuming Fiber...
Hello again from the Fiber...
Program exits...
Простой пример вывода echo, вероятно, не показывает преимуществ Fiber, потому что он не возвращает и не передает никаких значений. Поэтому возьмем более сложный пример, используя Fibers, сделаем копирование файлов с выводом процента готовности.
function writeToLog(string $message): void {
echo $message . "\n";
}
$files = [
'src/foo.png' => 'dest/foo.png',
'src/bar.png' => 'dest/bar.png',
'src/baz.png' => 'dest/baz.png',
];
$fiber = new Fiber(function(array $files): void {
foreach($files as $source => $destination) {
copy($source, $destination);
Fiber::suspend([$source, $destination]);
}
});
// Pass the files list into Fiber.
$copied = $fiber->start($files);
$copied_count = 1;
$total_count = count($files);
while(!$fiber->isTerminated()) {
$percentage = round($copied_count / $total_count, 2) * 100;
writeToLog("[{$percentage}%]: Copied '{$copied[0]}' to '{$copied[1]}'");
$copied = $fiber->resume();
++$copied_count;
}
writeToLog('Completed');
[33%]: Copied 'src/foo.png' to 'dest/foo.png'
[67%]: Copied 'src/bar.png' to 'dest/bar.png'
[100%]: Copied 'src/baz.png' to 'dest/baz.png'
Completed
Фактически операция копирования файлов выполняется внутри Fiber'a, а обратный вызов Fiber принимает только список файлов для копирования и их соответствующее место назначения.
После того, как файл скопирован, Fiber приостанавливается и возвращает имена источника и назначения обратно вызывающей стороне. Затем обновляется прогресс и регистрируется информация о только что скопированном файле.
Используя цикл while, Fiber возобновляется до тех пор, пока не завершится. Для Fiber возможно исключение Exception в случае, если он не может продолжать работать дальше, и далее идет возвращение в основное приложение.
Вы не будете использовать файберы напрямую
Согласно документации, Fibers предлагает «только минимум, необходимый для того, чтобы пользовательский код мог реализовать корутины с полным стеком или зеленые потоки в PHP».
Другими словами, если у вас нет очень странной причины использовать их напрямую, вам никогда не придется взаимодействовать с Fibers, как если бы вы выполняли корутины на Javascript или Go.
Некоторым высокоуровневым фреймворкам (например, Symfony, Laravel, CodeIgniter и Phalcon) потребуется некоторое время, чтобы понять, как подходить к Fibers и создать набор инструментов, с которыми они будут работать с точки зрения разработчика. Некоторые низкоуровневые фреймворки, такие как amPhp и ReactPHP, уже перешли на файберы в своих последних версиях разработки.
«Поскольку одновременно может выполняться только один файбер, у вас не будет наблюдаться race conditions, который может возникнуть при чтении или записи в памяти двумя потоками одновременно»
Именно фреймворки смогут решать проблемы параллелизма и синхронизации в одном и том же фрагменте памяти. Это хорошо, потому что вам не нужно думать о гонках данных, семафорах и мьютексах - вещах, которые гоферы прекрасно понимают.
Отсутствие каналов
Т.к. одновременно работает только один файбер, даже если вы объявляете несколько, то проблем с синхронизацией данных не будет. Но существует вероятность того, что еще один Fiber пробудится и перепишет то, что сделал первый файбер. Одно из решений - использовать стиль каналов Go.
С этой точки зрения Go намного опережает первоначальное решение для параллелизма PHP. Если вам нужно что-то полностью многопоточное, вы можете сделать свое программное обеспечение на Go или даже на Rust, если вы хотите напрямую использовать потоки ЦП.
Дело не в том, что PHP несовместим с какой-либо моделью параллелизма, но, безусловно его основа по-прежнему синхронна по своей сути за счет удобства и лучшей понятности.
Если нам нужна настоящая модель параллелизма, такая как в Go, тогда PHP придется переписывать с нуля, но это и откроет множество возможностей в вычислительном мире.
Влияние на обратную совместимость
Fiber Класс и его Throwables (FiberError/FiberExit) являются новыми в PHP 8.1. Хотя два Throwable можно тривиально заменен полифилами, сам класс Fiber с его поведением параллелизма не может быть перенесен в более старые версии PHP.
Любой код, объявляющий класс с именем Fiber в глобальном пространстве имен, вызовет фатальные ошибки из-за повторного объявления класса.
Дмитрий11.11.2023
думаю в примере с проталкиванием данных будет такой результат
Значение для suspend: вытолкнули
Значение для resume: втолкнули
Сергей Мухин 13.11.2023
Спасибо за внимательность! Поправил)