PHP и асинхронность. Такая комбинация долгие годы казалась невозможной, ведь PHP прочно ассоциировался с блокирующим подходом и синхронным выполнением скриптов «от запроса до ответа». С выходом PHP 8.1 ситуация несколько изменилась — появилась возможность реализовать асинхронность в PHP на основе файберов. Но есть нюанс — вопрос о том, действительно ли PHP с приходом файберов стал асинхронным, по-прежнему для многих остается открытым.
Меня зовут Михаил Сазонов. Я работаю в команде «Регистратура» в MedTech-компании №1 в России – в СберЗдоровье. В этой статье я разберу, наступило уже будущее или нет: стал ли PHP действительно асинхронным с приходом файберов или это миф.
Немного теории
Существует 3 варианта выполнения задач: в синхронном, параллельном и асинхронном режимах.
Синхронный режим подразумевает последовательное блокирующее выполнение.
В параллельном режиме задачи выполняются полностью параллельно, при этом внутри этих задач код может выполняться в синхронном блокирующем режиме.
В асинхронном режиме задачи выполняются последовательно неблокирующим образом — то есть, функции могут выполняться так, что они не будут блокировать выполнение других функций в тот момент, когда они чего-то ждут. То есть они могут запустить некую операцию, остановиться, отдать управление другим функциям, а когда результат будет готов, программа переключится обратно в эту функцию, и она продолжит свое выполнение с того места, где остановилась.
На практике необходимость в неблокирующем поведении чаще всего возникает при операциях ввода-вывода (I/O), которые по своей природе требуют ожидания ответа от внешних систем — например, при сетевых запросах или работе с устройствами.
Параллельные режимы или параллелизм, как правило, достигается либо запуском кода в отдельных процессах (многопроцессный подход), либо в отдельных потоках (многопоточный подход).
Асинхронность же достигается с помощью неблокирующего I/O и мультиплексирования I/O.
Таким образом, во время ожидания результата I/O-операции можно выполнять другие задачи.
Важно понимать, о каких именно задачах идет речь, поэтому внутри небольшое отступление
В классических PHP-приложениях, работающих, например, на основе php-fpm, каждый запрос — это отдельный процесс, внутри которого чаще всего выполняется некая блокирующая операция (например, HTTP или SQL-запрос). В результате до получения ответа от, допустим, базы данных нельзя начать выполнение следующих задач.
При этом полноценное асинхронное приложение — это не про асинхронные HTTP или SQL-запросы в рамках синхронного кода. Асинхронное приложение — это «неумирающее» PHP-приложение, в котором:
есть свой встроенный веб-сервер;
не нужен php-fpm;
выполняется самостоятельная обработка запросов.
Соответственно, работу условно «вечного» асинхронного приложения можно представить простой схемой-картинкой, где ядром системы является компонент event-loop, который управляет асинхронными задачами:

если одна задача приостановилась и чего-то ждет, event-loop переключает поток на следующий worker;
когда новый активный worker тоже останавливается, у первого уже готов результат и event-loop возвращает поток управления ему.
Примечание: Однако это лишь один из полюсов асинхронности. Подход, который мы разберем далее, не требует перехода на «вечное» приложение с собственным event-loop. Он позволяет точечно, внутри обычного синхронного приложения, использовать асинхронность для параллельного выполнения задач, практически не меняя существующую архитектуру и код. Файберы здесь выступают как инструмент для управления этим параллелизмом.
Выполнение множества задач в одном процессе приводит нас к концепции многозадачности в асинхронных программах.
Многозадачность в асинхронном коде
В разных языках многозадачность реализована по-своему, но можно выделить два подхода:
Кооперативная многозадачность — подход, когда ответственность за передачу управления лежит на самой выполняемой задаче. Это требует от функции явного вызова механизма для приостановки своего исполнения. Этот вид многозадачности «из коробки» характерен для PHP, Rust, Python, Javascript и других языков.
-
Вытесняющая многозадачность — подход, при котором вызывающий код сам решает, когда приостановить одну задачу и передать управление другой. Этот вид многозадачности, например, использует планировщик ОС и Go.
Примечание: Go, хоть и использует кооперативные горутины, применяет вытесняющее планирование на уровне своих потоков.
Оба подхода используют корутины — функции, которые могут приостанавливаться и передавать управление вызывающей функции.
В основе корутин в разных языках программирования могут лежать разные базовые низкоуровневые механизмы.
Это могут быть:
Коллбэки
Например, JavaScript и ReactPHP используют объекты Promise
Генераторы
В таких языках, как PHP, C#, JavaScript, Ruby и многих других есть генераторы, которые, как правило, создаются с использованием оператораyield
Особый синтаксис
Например, ключевые словаasyncиawaitв JavaScript или пометкиsuspendу функций в Kotlin
Нюанс в том, что все описанные варианты создают сложности под общим названием «цветные функции». Данное понятие - не официальный термин, а популярная метафора, которая помогает визуализировать ключевое различие между двумя стилями программирования: блокирующим (синхронным) и неблокирующим (асинхронным).
Представьте, что каждая функция в вашей программе имеет "цвет". Этот цвет определяется тем, блокирует ли выполнение этой функции поток или нет.
Так выделяют два "цвета" функций:
синий— синхронная функция;
красный— асинхронная функция.
Главное правило метафоры: синие функции могут вызывать как синие, так и красные, а красные могут вызывать только красные. В контексте функций асинхронные функции не могут напрямую вызывать синхронные блокирующие методы. Попытка сделать это приведет к блокировке и потере всех преимуществ асинхронности. Таким образом, асинхронный код "распространяется" вверх по стеку вызовов, требуя, чтобы все функции выше по цепочке также были асинхронными.

Помимо вышеперечисленных вариантов реализации корутин, можно использовать файберы.
Примечание: Файберы (Fibers) — это легковесные подпрограммы (корутины), реализующие кооперативную многозадачность внутри одного системного потока PHP. Они появились в PHP 8.1 как низкоуровневая основа для асинхронного программирования.
Главное преимущество файберов в том, что они относятся к stackfull-корутинам, то есть хранят весь стек вызовов. Благодаря этому они решают проблему "цветных" функций и могут полностью приостановить весь стек, независимо от того, как глубоко находится точка приостановки.
Теперь перейдем к деталям.
От теории к деталям: реализация корутин в PHP
Для работы с корутинами на базе файберов PHP предоставляет класс Fiber. С контрактом класса можно ознакомится здесь.
Примечательно, что контракт генератора во многом похож на файбер. Поэтому генераторы можно использовать как корутины.

Например, рассмотрим условный код, который выводит числа от 1 до 5 с помощью генератора.
function counter($n): Generator {
for ($i = 1; $i <= $n; $i++) {
yield $i;
}
}
$generator = counter(5);
while($generator->valid()) {
print_r($generator->current());
$generator->next();
}
Внутри функции counter вызывается оператор yield, который прерывает функцию внутри цикла на каждой итерации. После прерывания мы получаем то, что возвращает генератор, и выводим значение с помощью print_r.
Собственно прерываемая функция — это корутина. Причем, поскольку оператор yield меняет возвращаемый тип функции на Generator, можно увидеть пример «цветной» функции. То есть, везде, где вызывается этот метод, надо либо как-то обрабатывать генератор, либо тоже возвращать генератор.
Теперь к аналогичной реализации на файберах.
function counter($n): void {
for ($i = 1; $i <= $n; $i++) {
Fiber::suspend($i);
}
}
$fiber = new Fiber(fn() => counter(5));
print_r($fiber->start());
while($fiber->isSuspended()) {
print_r($fiber->resume());
}
Здесь:
вместо
yieldпоявилсяFiber::suspend(), который также какyieldсоздает точку прерывания;запуск самой функции
counterпроисходит не напрямую, а внутри callback, переданного в конструктор объектаFiber;в цикле while вместо next вызывается
Fiber::resumeдо тех пор, пока файбер не завершит работу и не перестанет приостанавливаться;
Но главное изменение в том, что функция counter больше не возвращает генератор — она возвращает тот тип, который она возвращала бы, если бы никакой точки приостановки не было бы. Таким образом файберы не окрашивают функции — это их основное преимущество по сравнению с генераторами.
Теперь разберем пример, более приближенный к реальности.
Асинхронные HTTP-запросы с использованием файберов и Guzzle (на основе PSR)
Для выполнения http запросов обычно используется http клиент одной из популярных библиотек и в последние годы считается правилом хорошего тона использовать PSR. Многие http-клиенты реализуют PSR стандарт, чтобы код бизнес-логики не зависел от реализации http-клиента.
Давайте разберём пример в котором с помощью файберов и Guzzle можно реализовать асинхронную отправку HTTP-запросов, практически не меняя существующий синхронный код.
Например, представим, у нас есть контроллер, который получает данные о пользователях с помощью сервиса UserService:
class Controller
{
private UserService $userService;
public function action() {
$user1 = $this->userService->getUser(1);
$user2 = $this->userService->getUser(2);
}
}
UserService в свою очередь получает данные с помощью PSR http-клиента. В нашем случае таким клиентом будет Guzzle.
class UserService
{
private \Psr\Http\Client\ClientInterface $client;
public function getUser(int $userId): UserDto {
$request = $this->createRequest('GET', '/api/get/' . $userId);
$response = $this->client->sendRequest($request);
return UserDtoFactory::createFromResponse($response);
}
}
Напоминание, как выглядит контракт PSR клиента
namespace Psr\Http\Client;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
interface ClientInterface
{
/**
* @throws \Psr\Http\Client\ClientExceptionInterface
*/
public function sendRequest(RequestInterface $request): ResponseInterface;
}Здесь есть нюанс. Guzzle предоставляет возможность отправки асинхронных запросов, но заставляет использовать объект Promise. Использование PSR клиента же не дает возможности менять возвращаемый тип. Иначе это приведет к изменению всего кода, который вызывается по цепочке. Поэтому надо либо отказываться от PSR и менять весь код, который уже может использоваться во многих местах приложения, либо каким-то образом сделать метод sendRequest асинхронным, но чтобы возвращаемый тип не поменялся. Здесь на помощь приходят файберы.
Чтобы понять, как можно решить текущую задачу, для начала стоит посмотреть на текущую реализацию метода PSR-интерфейса в Guzzle.
namespace GuzzleHttp;
class Client implements \Psr\Http\Client\ClientInterface
{
public function sendRequest(RequestInterface $request): ResponseInterface
{
$options[RequestOptions::SYNCHRONOUS] = true;
$options[RequestOptions::ALLOW_REDIRECTS] = false;
$options[RequestOptions::HTTP_ERRORS] = false;
return $this->sendAsync($request, $options)->wait();
}
}
Так, Guzzle использует метод sendAsync, который возвращает объект Promise и сразу вызывает метод wait, который отправляет запрос на выполнение и дожидается результата. Метод sendAsync не отправляет запрос сразу, он ставит его в очередь внутри Guzzle, а уже wait берёт все запросы из очереди и отправляет их с помощью функции curl_multi_exec.
Чтобы иметь возможность останавливать выполнение между постановкой в очередь и отправкой запросов, можно создать отдельный декоратор над Guzzle клиентом, который перепишет метод sendRequest.
namespace GuzzleHttp;
class ClientDecorator implements \Psr\Http\Client\ClientInterface
{
public __construct(private GuzzleHttp\Client $guzzle) {
}
public function sendRequest(RequestInterface $request): ResponseInterface
{
$options[RequestOptions::ALLOW_REDIRECTS] = false;
$options[RequestOptions::HTTP_ERRORS] = false;
$promise = $this->guzzle->sendAsync($request, $options);
if (Fiber::getCurrent() !== null) {
Fiber::suspend();
}
return $promise->wait();
}
}
Причем хватит незначительных изменений по сравнению с оригинальным методом Guzzle. Так, достаточно разъединить цепочку вызовов — по-прежнему будет вызываться метод sendAsync, но теперь перед вызовом wait будет выполняться проверка на факт нахождения внутри файбера (если да — мы останавливаем его).
Важно, что на этом этапе существующий код абсолютно ничего не почувствует и не сломается, потому что все функции в этот момент запускаются не внутри файберов. Метод Fiber::getCurrent в данный момент всегда будет возвращать null, и мы по-прежнему будем выполнять запросы синхронно.
Теперь попробуем сделать так, чтобы запросы начали отправляться асинхронно.
class Controller
{
private UserService $userService;
public function action() {
[$user1, $user2] = Await::all([
fn() => $this->userService->getUser(1),
fn() => $this->userService->getUser(2),
]);
}
}
Здесь всё также довольно просто — достаточно обернуть методы, которые должны выполняться асинхронно, в функцию-хелпер Await::all, которая сделает из них файберы и запустит по очереди.
Посмотрим немного подробнее на то, что именно делает функция Await::all:
class Await
{
public static function all(array $callableList): array
{
// Создаем массив результатов, в который будем записывать результаты выполнения замыканий.
// Это гарантирует, что порядок результатов будет соответствовать порядку переданных замыканий.
$results = array_fill_keys(array_keys($callableList), null);
// Создаем файберы из наших замыканий и стартуем каждый.
// После старта они могут остановиться при вызове Fiber::suspend в них.
/** @var array<int|string, array{fiber: Fiber<null, mixed, mixed, mixed>, suspendValue: mixed}> $suspendedFibers */
$suspendedFibers = [];
foreach ($callableList as $key => $callable) {
$fiber = new Fiber($callable);
$suspendValue = $fiber->start();
if ($fiber->isTerminated()) {
$results[$key] = $fiber->getReturn();
continue;
}
$suspendedFibers[$key] = [
'fiber' => $fiber,
'suspendValue' => $suspendValue,
];
}
// Если все файберы завершили работу(не приостанавливались), то возвращаем результаты.
if (count($suspendedFibers) === 0) {
return $results;
}
// Когда все файберы стартанули и остановились продолжаем их выполнение.
foreach ($suspendedFibers as $suspendedFiber) {
$suspendedFiber['fiber']->resume($suspendedFiber['suspendValue']);
}
// После того как файберы закончили свою работу, получаем их результаты, проверяя, что они завершились.
foreach ($suspendedFibers as $key => $queueItem) {
// Если файбер не завершился, то выбрасываем исключение.
if (!$queueItem['fiber']->isTerminated()) {
throw new FiberNotTerminatedException();
}
$results[$key] = $queueItem['fiber']->getReturn();
}
return $results;
}
}
Примечание: Пример упрощен и не включает обработку исключений. В продакшн-решении необходимо корректно обрабатывать ошибки в файберах, чтобы аварийное завершение одного из них не нарушило работу всего цикла событий.
Что происходит в коде выше?
Функция
allпринимает в себя массив замыканий, дальше в цикле создает из каждого замыкания файбер и запускает его.Запуская файбер, мы начинаем выполнение переданной функции — она проходит всю цепочку вызовов и в конце доходит до метода
sendRequest в декораторе.В декораторе вызывается метод
sendAsync, запрос ставится в очередь на выполнение и дальше с помощью методаFiber::getCurrentпроверяется, находимся мы в файбере или нет.Поскольку мы в файбере, эта функция возвращает нам текущий файбер.
Далее выполняется вызов
Fiber::suspend, что полностью останавливает весь стек выполнения файбера.После остановки нас выбрасывает туда, откуда был запущен наш файбер — в функцию-хелпер, то есть место, где был вызван метод
start.Файбер сохраняется в массив, и цикл продолжается.
Со вторым замыканием происходит то же самое. Таким образом, к концу первого цикла мы имеем в массиве два файбера, которые дошли до декоратора, вызвали оба метод
sendAsync, поставив два запроса на выполнение в очередь, и приостановились.Далее нужно продолжить файберы, чтобы запросы отправились. Для этого запускается цикл по файберам и вызывается метод
resumeу каждого. Первый файбер после вызова этого метода продолжает выполнение с того момента, где он приостановился, и вызывает методwaitу промиса, который он получил с помощью методаsendAsyncранее.Так как к этому моменту в очереди стоит два запроса, метод
waitотправит их оба на выполнение. То есть, в этот момент мы уже отправили два запроса "одновременно".Далее дожидаемся, когда нужный нам запрос отработает и функция
sendRequestсможет вернуть результат работы в виде полноценногоResponse.Следом выполняется вся цепочка вызовов до тех пор, пока мы снова не вернемся в хелпер.
Со вторым файбером произойдет тоже самое. В конце нам нужно получить результаты работы замыканий, из которых мы создавали файберы. Для этого мы пробегаемся по файберам и получаем результаты с помощью метода
getReturn.
Поскольку файбер создается в начале цепочки вызовов и останавливается в декораторе перед самой отправкой запроса, другие функции даже не будут знать, что находятся внутри файбера.
Таким образом, добавив немного кода, можно реализовать возможность отправлять асинхронные http-запросы. Причем таким образом, чтобы весь промежуточный код об этом не знал.
Примечание: Представленный пример — обзорный. Например, он не учитывает, что файбер может быть приостановлен более одного раза. Но даже его достаточно, чтобы донести суть концепции. Представленная функция Await::all — это не полноценный event-loop. Ее единственная задача — координировать группу файберов: запустить их всех, дождаться приостановки каждого из-за ожидания I/O, а затем массово возобновить. Этот подход идеально ложится на модель работы Guzzle с его очередью запросов и curl_multi_exec. Таким образом, нам удается добиться параллелизма для группы HTTP-запросов, не вводя в приложение глобальный цикл событий и не ломая его синхронную архитектуру. Для истинного non-blocking I/O потребовались бы асинхронные драйверы, написанные, например, на основе расширения ext-event или ext-ev.
Выводы
Асинхронный PHP — реальность. Поскольку уже сейчас есть много возможностей, решений и библиотек для реализации асинхронного подхода под разные сценарии.
Файберы — это лишь один из фрагментов пазла, который нужен для реализации асинхронности, но недостаточный для полноценного асинхронного приложения. Однако они уже сегодня помогают решать конкретные задачи по распараллеливанию операций внутри обычного синхронного приложения. Это мощный инструмент, который требует минимальных изменений в коде и не заставляет вас отказываться от привычного стека технологий.
Безусловно, в перспективе использование асинхронного PHP сможет дать значимый профит, поскольку такое приложение будет иметь меньше соединений с БД, потреблять меньше ресурсов, легче в развертывании, позволит, например, в некоторых сценариях отказаться от Redis для кэширования данных в пользу in-memory кэша.
Но, по моему мнению, до раскрытия полного потенциала асинхронного PHP нужно подождать еще несколько лет.
Источники используемые при написании статьи
При написании данной статьи я вдохновлялся докладом Вадима Занфира "Чем вам не угодил асинхронный PHP" на PHPRussia 2024 и интервью Валентина Удальцова на канале Пых с автором инициативы PHP True Async.
Также я использовал статью Боба Найстрёма "What color is your function?"
Комментарии (0)

des1roer
18.09.2025 08:15А сравнивали с тем же го? Было необходимо асинхронно запросить несколько ендпоинтов за раз - газл даже в асинхронном режиме сильно проигрывает горутинам

black_cat Автор
18.09.2025 08:15Конкретно это решение с Go не сравнивали, так как не было такой задачи.
Решение описанное в статье - это попытка добавить асинхронность для http-запросов в уже существующие сервисы не сильно меняя код, архитектуру и языки программирования)

eee
18.09.2025 08:15А существуют ли какие нибудь удобные библиотеки, чтобы было не так низкоуровнево?

mad_maximus
18.09.2025 08:15revolt/event-loop, amphp/{http-client, http-server, mysql, postgres, redis}, thesis/{amqp, nats}.

mad_maximus
18.09.2025 08:15У вас неправильно работает
Await::all. Сейчас вы просто продолжаете каждый файбер после запуска, даже если ответ на запрос еще не пришел. Смысл вFiber::suspendиFiber::resumeв том, чтобы их вызвать в определенное время. Например,Fiber::suspendвызвать после того, как мы отправили запрос по сети, аFiber::resumeпосле того, как мы узнаем, например черезstream_select, что ответ пришел – и мы можем продолжить корутину дальше, чтобы она могла прочитать данные из сокета.
black_cat Автор
18.09.2025 08:15Действительно, можно сделать так как вы говорите, но для решения задачи, которая перед нами стояла это не требуется. Смысл функций suspend и resume в том, чтобы приостановить файбер, а когда это сделать (после отправки запроса по сети или после постановки в очередь, но до отправки по сети) - это уже дело конкретной реализации. В нашем случае с Guzzle есть возможность сделать так как приведено, потому что Guzzle использует
curl_multi_exec. Для других задач, конечно может потребоватьсяstream_selectили что-то подобное и функцияAwait::allпредставленная в статье будет непригодна.В примечании к функции
Await::allя специально это отметил.
mad_maximus
18.09.2025 08:15Действительно, можно сделать так как вы говорите, но для решения задачи, которая перед нами стояла это не требуется
Я не понимаю, какая перед вами стояла задача и почему вы считаете текущий код решением. Вы просто не используете ровно никаких преимуществ файберов. Вы сначала приостановили код, а потом сразу же запустили опять, хотя промис газзла мог быть еще не готов к завершению. Чтобы код имел смысл, вы должны узнать, что промис завершился, и тогда вызывать
Fiber::resume.
black_cat Автор
18.09.2025 08:15Вы можете поставить газзл, взять пример из статьи и убедиться, что 2 запроса будут отправлены одновременно.
У меня в статье написано следующее:
Метод
sendAsyncне отправляет запрос сразу, он ставит его в очередь внутри Guzzle, а ужеwaitберёт все запросы из очереди и отправляет их с помощью функции curl_multi_execПопытка объяснить «на пальцах»: Файбер приостанавливается после добавления первого запроса в очередь газзла. После этого запускается второй файбер который делает тоже самое и также приостанавливается. К этому моменту в очереди газзла 2 неотправленных запроса. После, первый файбер продолжается вызывая метод wait у первого промиса. Вызов wait запускат отправку ВСЕХ запросов, которые есть в очереди газзла, независимо от того у какого промиса этот wait вызван. В этот момент поток фактически блокируется, пока нужный промис не разрешится. Далее, когда промис разрешился файбер доходит до конца. Следом происходит продолжение выполнения второго файбера, у второго промиса также вызывается метод wait. Так как в очереди в этот момент нет запросов на отправку, метод wait либо сразу же вернет результат (если он уже готов), либо дождется пока результат нужного промиса придёт.
Вот так это работает. Вы можете сами это проверить скопировав пример метода Await::all из статьи.

black_cat Автор
18.09.2025 08:15Я не ответил на вопрос какая стояла задача.
Задача была добавить асинхронность для http-запросов в уже существующие сервисы не сильно меняя код и архитектуру.

tkutru
18.09.2025 08:15По поводу теории - (а)синхронность, параллелизм и многозадачность свалены в кучу.
Правильнее разделять так:(А)синхронность.
PHP по дефолту синхронный, то есть код исполняется последовательно, в порядке вызова.
Асинхронное выполнение означает, что каждая функцая (метод, со-программа) может вызываться и отрабатывать (завершать работу) не в том порядке, в каком была вызвана.
Что касается цитатыPHP и асинхронность. Такая комбинация долгие годы казалась невозможной, ведь PHP прочно ассоциировался с блокирующим подходом и синхронным выполнением скриптов «от запроса до ответа»
Такая комбинация всегда была возможной через написание менеджера вызовов и обработки по типу event loop как в javascript (js оказывается под капотом тоже синхронный!). Но до поры до времени мейнтейнерам пыхи хватало ума держаться подальше от такой асинхронности.
Многозадачность (конкурентность).
Многозадачность это способ разделить ресурсы между со-программами так, чтобы всем досталось понемногу. Например, процессорное время по какому-то правилу распределяется между со-программами (или процессами), таким образом каждая со-программа за каждую секунду работы получает какое-то процессорное время. Вытесняющая многозадачность это когда правило распределения - условно, пропорция (каждой со-программе даем столько-то квантов, % процессорного времени), а кооперативная - когда со-программа отрабатывает и сама освобождает ресурс для следующей ожидающей со-программы.Параллелизм.
Это когда со-программы/процессы выполняются параллельно, то есть без прямой зависимости друг от друга. Например, на разных компьютерах или на разных ядрах одного компьютера.
Обратите внимание, можно реализовать (а)синхронность или многозадачность и на одном ядре, но параллелзим возможен только при наличии более чем одного вычислительного узла (например ядра проца).

mavir
18.09.2025 08:15Только Fiber это всего лишь синтаксический сахар, а не асинхронщина в классическом его смысле. Не будь у нее асинхронных вызовов, например, curl_multi_exec или pg_send_query вы бы не добились одновременно выполнения двух запросов, например, через curl или PDO.
То есть они могут запустить некую операцию, остановиться, отдать управление другим функциям, а когда результат будет готов, программа переключится обратно в эту функцию, и она продолжит свое выполнение с того места, где остановилась.
Т.е. Fiber просто заменяет цикл while. Разве что код немного красивее становится

mad_maximus
18.09.2025 08:15Файберы не заменяют циклы. Файберы (stackfull корутины) заменяют генераторы (stackless корутины) в способе переключения контекста. Цикл вокруг select/epoll/kqueue остается.
FanatPHP
Статья - просто бомба! Использование (относительно) новой фичи в РНР, а не пережёвывание старого. Практически реальный, а не совсем академический пример. Толковая подача. Спасибо большое! Всем корпоративным блогам брать пример со СберЗдоровья!