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

Digital-студия WNDER

Почему PHP и асинхронность — это боль?

Представьте, что вы в кафе. Официант (PHP) подходит к столику, принимает заказ и идёт на кухню. Пока повар готовит, официант стоит и ждёт. Именно так работает классический PHP: каждый запрос блокирует процесс до полного выполнения. Если в коде есть запрос к внешнему API, который отвечает 2 секунды, всё это время скрипт просто спит.

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

Главная проблема в том, что PHP изначально проектировался как синхронный язык. Все стандартные функции — file_get_contents, curl_exec, sleep — блокируют выполнение. Чтобы добавить асинхронность, нужно либо использовать расширения (например, Swoole), либо строить событийный цикл на пользовательском коде (ReactPHP, Amp).

Основные подходы к асинхронности в PHP

Есть несколько способов заставить PHP работать асинхронно. Каждый со своими тараканами.

  • Многопроцессность (pcntl_fork) — каждый запрос обрабатывается в отдельном дочернем процессе. Просто, но дорого по памяти и не всегда доступно на shared-хостинге.
  • Событийный цикл (ReactPHP, Amp) — библиотеки, которые реализуют неблокирующий ввод-вывод через stream_select. Вы пишете код с колбэками или промисами.
  • Корутины (Swoole, Fiber) — позволяют писать асинхронный код так, будто он синхронный. Под капотом переключаются контексты выполнения.
  • Очереди (RabbitMQ, Redis) — выносим тяжёлые задачи в фоновые воркеры. Это не совсем асинхронность в рамках одного запроса, но часто именно то, что нужно.

Для наглядности сравним два популярных инструмента: ReactPHP и Swoole.

Критерий ReactPHP Swoole
Установка Через Composer, без расширений Требуется установка PHP-расширения
Стиль кода Промисы и колбэки Корутины, похоже на синхронный код
Производительность Средняя, зависит от event loop Высокая, компилируется в C
Поддержка HTTP/2 Есть Есть
Порог входа Низкий Средний (нужно ставить расширение)

Пример асинхронного запроса на ReactPHP

Допустим, нам нужно одновременно запросить данные с трёх разных API. Синхронно это займёт суммарное время всех запросов. С ReactPHP мы запустим их параллельно и дождёмся всех результатов.

get($url);
}

React\Promise\all($promises)->then(function ($responses) {
    foreach ($responses as $response) {
        echo $response->getBody() . PHP_EOL;
    }
});

$loop->run();

Здесь мы создаём событийный цикл, инициируем три GET-запроса одновременно и ждём, пока все они завершатся. Время выполнения будет примерно равно самому долгому запросу, а не их сумме.

Корутины в Swoole: асинхронность без боли

Swoole предлагает корутины — это как потоки, но легче. Вы пишете обычный синхронный код, а внутри происходит магия переключения. Например, так выглядит параллельный опрос двух сервисов:

push($result);
});

Coroutine::create(function () use ($channel) {
    $result = file_get_contents('https://api.example.com/posts');
    $channel->push($result);
});

for ($i = 0; $i < 2; $i++) {
    echo $channel->pop() . PHP_EOL;
}

Обратите внимание: мы используем обычный file_get_contents, но внутри корутины он не блокирует другие корутины. Swoole перехватывает блокирующие вызовы и делает их неблокирующими на уровне ядра.

Правило: не смешивайте блокирующие и неблокирующие вызовы в одном событийном цикле. Если вы используете ReactPHP, все операции ввода-вывода должны быть асинхронными. Один синхронный file_get_contents остановит весь цикл.

Практические советы и подводные камни

Асинхронность в PHP — это не серебряная пуля. Вот несколько рекомендаций, чтобы не наступить на грабли.

  • Не тащите асинхронность везде. Если у вас обычный сайт с низкой нагрузкой, синхронный PHP работает отлично. Асинхронность нужна для высоконагруженных API, websocket-серверов и микросервисов.
  • Следите за утечками памяти. В долгоживущих процессах (Swoole, ReactPHP) память не освобождается после каждого запроса, как в классическом PHP. Глобальные переменные и статические массивы могут расти бесконечно.
  • Используйте пулы соединений. Асинхронные клиенты к базам данных (MySQL, Redis) должны переиспользовать соединения, иначе вы быстро исчерпаете лимиты.
  • Тестируйте под нагрузкой. Асинхронный код сложнее отлаживать. Обязательно пишите юнит-тесты и проводите нагрузочное тестирование (например, с помощью wrk или ab).

Ещё один важный момент: асинхронность не отменяет очереди. Если задача может выполняться минутами (отправка email, генерация отчёта), лучше вынести её в фоновый воркер через RabbitMQ или Redis. Асинхронность в рамках запроса хороша для коротких операций ввода-вывода.

Что в итоге?

PHP давно перестал быть только синхронным языком. С появлением Swoole, ReactPHP и встроенных Fiber у разработчиков есть инструменты для создания высокопроизводительных асинхронных приложений. Но выбирать их стоит осознанно: если у вас нет задач, где нужна параллельная обработка тысяч соединений, возможно, вам достаточно обычного PHP с очередями.

Главное — понимать, как работает событийный цикл, не блокировать его и следить за ресурсами. Тогда асинхронность станет вашим другом, а не источником багов.

Студия WNDER