Александр Некрасов · Автономное руководство

PHP: полный мануал с примерами, функциями и ООП

Разделы этой страницы
  1. Fibers: приостановить функцию и продолжить с того же места
  2. Сначала условия: когда эта тема вообще нужна
  3. Какая операция кому передаёт управление
  4. Минимальный обмен: запрос цены → 1200 → 2400
  5. Пауза внутри обычной вложенной функции
  6. Состояние важнее догадки «наверное, уже готово»
  7. Почему это ещё не параллельность и не очередь заданий
  8. Самостоятельный опыт без настоящего ожидания
  9. Код и руководство с собой
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 53 · Продвинутое продолжение

Fibers: приостановить функцию и продолжить с того же места

Продвинутый механизм PHP 8.1+: передаём управление и результат без обещания автоматической параллельности.

Сначала условия: когда эта тема вообще нужна

Наглядная модель

Пауза и продолжение одного расчёта

Задача считает две упаковки, но сначала просит цену. Управляющий код отвечает числом 1200.

  1. start()

    Начинаем функцию. Она доходит до запроса цены.

  2. suspend()

    Наружу приходит «Нужна цена tea»; функция временно остановлена.

  3. resume(1200)

    Ответ становится значением unitPrice; расчёт продолжается.

  4. getReturn() → 2400

    После return читаем стоимость двух упаковок в копейках.

Стрелки показывают порядок передачи управления. Одновременных потоков, сети и настоящего ожидания в этом примере нет.

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

Это продвинутая глава. До неё нужны вызовы и return, замыкания, объекты и методы и исключения. Для обычной формы или последовательного скрипта Fiber не обязателен. Класс появился в PHP 8.1; примеры используют базовую функциональность, совместимую с PHP 8.2+.

Каждый пример запускается отдельно. Скачайте его архив, распакуйте целиком, откройте терминал в этом каталоге и выполните php example.php. Сравните вывод с блоком «Ожидаемый результат». Не объединяйте примеры в один файл: некоторые намеренно повторяют имена функций и классов.

Какая операция кому передаёт управление

$fiber = new Fiber(static function () { /* работа */ });
$request = $fiber->start();
$fiber->resume($answer);
$result = $fiber->getReturn();
ЗаписьЧто происходит
new Fiber(функция)Создаётся объект с задачей; тело функции ещё не выполняется.
$fiber->start()Первый запуск. Управление вернётся при suspend или завершении.
Fiber::suspend($request)Внутри Fiber: отдать request вызывающему коду и приостановиться.
$fiber->resume($answer)Снаружи: продолжить приостановленный Fiber; answer станет результатом suspend внутри.
return $resultЗавершить функцию Fiber. Это отличается от временного suspend.
$fiber->getReturn()Забрать окончательный результат после нормального завершения.

Стрелка -> вызывает метод у конкретного объекта, двойное двоеточие :: здесь вызывает статический метод класса. start и resume возвращают значение следующего suspend либо null при завершении функции. По одному null нельзя строить весь протокол: suspend тоже может передать null; учитывайте состояние Fiber.

Минимальный обмен: запрос цены → 1200 → 2400

Приостановить расчёт до получения ценыСамостоятельный пример
<?php
declare(strict_types=1);

$fiber = new Fiber(static function (): int {
    $unitPrice = Fiber::suspend('Нужна цена tea');
    if (!is_int($unitPrice) || $unitPrice < 0) {
        throw new InvalidArgumentException('Ожидалась цена в копейках');
    }
    return $unitPrice * 2;
});

echo $fiber->start(), "\n";
echo 'Приостановлен: ', $fiber->isSuspended() ? 'да' : 'нет', "\n";
$fiber->resume(1200);
echo 'Стоимость двух упаковок: ', $fiber->getReturn(), " коп.\n";
Ожидаемый результат
Нужна цена tea
Приостановлен: да
Стоимость двух упаковок: 2400 коп.

Конструктор сохраняет функцию, start начинает её выполнение. suspend отдаёт наружу запрос цены и останавливает эту функцию. Число из resume(1200) становится результатом вызова suspend внутри неё. return завершает расчёт; итог читается getReturn, а не берётся из возвращаемого значения resume. Сети и настоящего ожидания здесь нет.

Пауза внутри обычной вложенной функции

Вложенная функция запрашивает остаток, управляющий код даёт ответСамостоятельный пример
<?php
declare(strict_types=1);

function readAvailableUnits(string $sku): int
{
    $response = Fiber::suspend(['operation' => 'stock', 'sku' => $sku]);
    if (!is_int($response) || $response < 0) {
        throw new UnexpectedValueException('Ожидался неотрицательный остаток');
    }
    return $response;
}

function stockLine(string $sku): string
{
    $units = readAvailableUnits($sku);
    return $sku . ': ' . $units . ' в наличии';
}

$fiber = new Fiber(static fn (): string => stockLine('tea'));
$request = $fiber->start();
$fixture = ['tea' => 4];
echo 'Запрос остатка: ', $request['sku'], "\n";
$fiber->resume($fixture[$request['sku']]);
echo $fiber->getReturn(), "\n";
Ожидаемый результат
Запрос остатка: tea
tea: 4 в наличии

Пауза произошла глубже: Fiber → stockLine → readAvailableUnits. Стек этих вызовов сохранился, а после resume функции продолжились с прежнего места. Управляющий код вручную взял ответ из локального массива. Это учебный обмен сообщениями, не event loop и не клиент склада. readAvailableUnits здесь допустимо вызывать только из выполняющегося Fiber.

В этом и отличие от простого «вернуть специальное значение»: не нужно вручную восстанавливать все промежуточные локальные переменные. Однако кто-то всё равно обязан получить ответ и вызвать resume. В примере это код после start; в асинхронной библиотеке — согласованный механизм выполнения задач.

Состояние важнее догадки «наверное, уже готово»

Не продолжать незапущенный или завершённый FiberСамостоятельный пример
<?php
declare(strict_types=1);

$fiber = new Fiber(static function (): string {
    Fiber::suspend('жду');
    return 'готово';
});

try {
    $fiber->resume();
} catch (FiberError) {
    echo "До start: FiberError\n";
}
$fiber->start();
try {
    $fiber->getReturn();
} catch (FiberError) {
    echo "До завершения: FiberError\n";
}
$fiber->resume();
echo 'Итог: ', $fiber->getReturn(), "\n";
try {
    $fiber->resume();
} catch (FiberError) {
    echo "После завершения: FiberError\n";
}
Ожидаемый результат
До start: FiberError
До завершения: FiberError
Итог: готово
После завершения: FiberError

resume разрешён только при приостановке. getReturn требует нормального завершения функции. Исключения здесь перехвачены для наблюдения состояния; в библиотеке неверный порядок вызовов лучше не создавать. Повторный start также недопустим. После необработанного исключения внутри Fiber завершённого результата для getReturn нет.

suspend вне выполняющегося Fiber вызывает FiberError. Исключение из задачи, которое не было перехвачено внутри, выходит через start или resume к управляющему коду. Продолжать завершённую задачу нельзя: создайте новый Fiber для новой работы, если протокол допускает повтор.

Почему это ещё не параллельность и не очередь заданий

Если внутри Fiber вызвать блокирующее чтение, HTTP-клиент или sleep, выполнение PHP будет ждать эту операцию. Fiber не переписывает её в неблокирующую. Для конкурентного ожидания нужны совместимые операции ввода-вывода и event loop — цикл, который отслеживает готовность событий и возобновляет нужные задачи. Выбор и настройка такой библиотеки выходят за рамки этого опыта.

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

Если нужно выдавать элементы по одному, начните с генератора и yield. Если нужна обычная функция расчёта — используйте её. Fiber оправдан, когда требуется именно управляемая пауза в стеке вызовов.

Самостоятельный опыт без настоящего ожидания

Попробуйте сами. В первом примере передайте в resume цену 900. Каким станет результат? Затем замените её строкой "900": где окажется ошибка?

Разбор и проверка

Число 900 даст 1800 копеек. Строка не проходит is_int, задача выбросит InvalidArgumentException, которое выйдет из resume. Перехватывать его нужно вокруг resume; вызывать после этого getReturn как для успешного итога нельзя. Этот опыт проверяет контракт передаваемых данных, не скорость исполнения.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.