Verification: a143cc29221c9be0

Php callback function with arguments

Php callback function with arguments

Типизация в PHP

В языке PHP динамическая типизация. Это значит, что при инициализации переменной ей нельзя назначить тип. Тип переменной выставиться динамически в зависимости от значения (или результат выполнения выражения), которое ей присваивается. Он может меняться в процессе выполнения скрипта. Продемонстрирую на примере:

Для справки: есть языки программирования со статической типизацией. В них при инициализации переменной нужно обязательно указывать тип. В случае, если переменной будет присваиваться значение типа, отличного от указанного, то компилятор сообщит об ошибке.

Типизация аргументов в функциях и методах

В последних версиях PHP появился возможность указывать типы для аргументов функций и возвращаемого ею значения. Рассмотрим эти возможности на примере:

Код содержит функцию с 3 аргументами различных типов. Типы указываются перед именем аргумента, а тип возращаемого значения после двоеточия.

Обратите внимание на строку «declare(strict_types=1);» Она включает строгую типизацию. Например, если при строгой типизации мы передадим в первый аргумент функции строку вместо числа, то получим ошибку:

TypeError: Argument 1 passed to myTestOperation() must be of the type int, string given

Если удалить строчку declare(strict_types=1), PHP попытается преобразовать переданную строку в число. Если преобразовать невозможно, PHP выбросит исключение TypeError.

Типы аргументов и тип возвращаемого значения могу быть следующими:

  • int - целочисленный тип;
  • float - числа с плавающей точкой (десятичная дробь);
  • string - строка;
  • array - массив;
  • callback - Callback-функция;
  • имя класса - в качестве аргумента должен передаваться экземпляр класса;
  • интерфейс - в качестве аргумета должен передаваться класс, имплементирующий (реализующий) данный интерфейс.

Null и void в сигнатуре функции

В случае, если функция или метод класса не возвращают значение, после двоеточия пишется ключевое слово void.

function myAction(): void 
{
    // ...
    // В функции не должно быть return
}

В случае, если какие-то из аргументов функции мы захотим сделать необязательными, на нужно добавить знак ? перед именем типа. Это правило работает и для возвращаемого значения. Например, для аргумента ?int $myVar можно передать значение либо null, либо целое число. Чтобы явно не передавать null в качестве значения необязательного аргумента, обычно в сигнатуре присваивают значение по умолчанию, как показано в следующем примере:

function myTestOperation(int $firstParam, ?string $secondParam = null, ?array $thirdParam = null): ?float
{
    var_dump($firstParam); // выведет int(6)
    var_dump($secondParam); // выведет NULL
    var_dump($thirdParam); // выведет NULL

    return null; // Можно венруть null, так как в сигнатуре ?float (либо десятичная дробь, либо null)
}

// Вызываем функцию
// Явно передаем null во 2 и 3 аргументы
myTestOperation(6, null, null);

// Вызываем еще раз
// При этом не передаем необязательные аргуметы, так как в сигнатуре присвоены дефолтные значения
// При этом результат выполнения будет идентичен
myTestOperation(6);

Преимущества указания типов в PHP

Использование типов и строгой типизации дает на следующие преимущества:

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

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

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

В свое время в 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 придется переписывать с нуля, но это и откроет множество возможностей в вычислительном мире.