php синхронный или асинхронный
Работаем асинхронно в PHP или история ещё одного чата
Меня очень радует, как бурно развивается PHP последние несколько лет. Наверное и вас тоже. Появляются постоянно новые возможности, удерживающие энтузиастов оставаться на данной платформе. Чего только стоит недавняя новость о релизе Hack.
Наверняка кто-то прочитав даже заголовок этой статьи ухмыльнется и подумает: «Мсье знает толк в извращениях!». Споры о крутости того или иного языка никогда не утихают, но как бы там ни было, лично я для себя вижу не так уж и много условий смены языка, поскольку люблю выжимать все возможности, прежде чем радикально сменить весь стек. Недавно была публикация о создании чата на Tornado и мне захотелось рассказать о том, как похожую задачу я решал при помощи PHP.
Предыстория
В один прекрасный день решил я познакомиться с WebSockets. Меня заинтриговала технология, хотя не сказать бы, что она появилась только вчера, и это совпало с запуском одного чат-сервиса соционической тематики, который страдал массой недостатков. Это придало мне азарт принять участие в конкурентной гонке. Использование веб-сокетов выглядело принципиально новым и многообещающим решением.
Соединение устанавливается постоянным и двунаправленным, а на стороне клиентской части работа сводится к обработке 4-х событий: onopen, onclose, onerror и конечно же onmessage. Никаких больше запросов через setInterval, избыточного трафика и нагрузки на сервер.
Позвольте здесь сделать небольшое отступление для тех, кто не понимает о чём речь.
Те, кто знаком с рунетом начала 2000-х может помнят многообразие чат-сервисов, где всё тормозило и неуклюже работало.
Чуть позже появился AJAX и стало гораздо лучше, однако в сущности принцип не изменился. Клиентская часть всё так же с некоторой заданной частотой по таймеру опрашивала сервер, разве что теперь можно было отказаться от использования iframe и снизить немного нагрузку на сервер за счёт меньшего объема отдаваемых данных.
Собственно, упомянутый чат-сервис был классическим ajax-чатом.
Те, кто никогда не слышал о том, что такое «резидентная программа», а писал лишь код для web-страницы, работающий по принципу «запустился-отработал-умер», испытывают разрыв шаблона при написании демона в первый раз. Например, выясняется, что инстанцированные объекты могут «жить» долго и хранить информацию без использования хранилища типа базы данных, и доступ к которой можно получать из разных подключений к демону. Пожалуй именно при его написании наиболее остро можно натолкнуться на проблему блокирующих функций и просто отсутствия заточенности PHP под асинхронность.
Что есть вообще асинхронность? Если по-простому, то это способность кода «распараллеливаться», выполнять несколько кусков кода независимо друг от друга.
UPD: alekciy справедливо заметил:
Асинхронность — возможность непоследовательного выполнения кода. Параллельность — возможность выполнения одного и того же кода в одновременно.
Я надеюсь, что читатель знаком хотя бы с азами JavaScript. Большинство хоть раз писали нечто вроде:
Элементарно, да? Определяется обработчик события клика на какой-то элемент страницы. А что, если мы попробуем нечто подобное сделать в PHP?
Первый вопрос возникнет «где определить события объекта». Второй «как сделать так, чтобы постоянно происходил опрос объекта на данное событие?». Ну допустим, мы сделаем некий бесконечный цикл, в котором будет опрашиваться данное событие. И тут же столкнемся с рядом серьёзных ограничений. Во-первых, частота опроса не должна быть слишком низкой, чтобы реакция системы была удовлетворительной. И не должна быть слишком высокой, чтобы не создавать проблем с нагрузкой на систему. Во-вторых, когда событий станет несколько, возникнет проблема с тем, что пока первый обработчик не отработает — другой не начнёт свою работу. А если надо обрабатывать тысячи подключений одновременно?
Но на сцене появляется ReactPHP и делает магию.
Ингридиенты
Краткая структура
Серверная часть состоит из двух ассиметричных по размеру частей кода: кассические web-страницы (index, восстановление пароля) и демон чат-сервиса.
Главная страница решает задачи загрузки клиентского веб-приложения, а также инициализацию сессии.
Демон представляет собой в основе реализацию интерфейса MessageComponentInterface из пакета Ratchet в виде класса MyApp\Chat. Реализуемые методы обрабатывают события onOpen, onClose, onError и onMessage.
Каждый из обработчиков, за исключением onError, представляет собой шаблон Chain-of-Responsibility. Наиболее объемный кусок кода пришёлся на onMessage, где он декомпозирован на контролеры.
Возникшие проблемы и способы решения
Что сейчас умеет чат и чему ещё научится
Какие выводы можно сделать по прошествии 2-х месяцев разработки проекта? У PHP всё ещё есть потенциал. По крайней мере начало работы с событийно-ориентированной парадигмой положено. Но увы, пока что язык пытается догнать, а не стать во главе движения. Если сравнить Ratchet и Tornado, то по возможностям они ещё не ровня. Будем надеяться, что развитие в этом направлении продолжится с положительным ускорением.
Для любопытных, исходный код проекта можно увидеть здесь.
Конструктивные комментарии приветствуются.
PHP Profi
Квест → Как хакнуть форму
Асинхронный PHP: Зачем? Перевод
Асинхронное программирование сегодня достаточно востребовано. Особенно в веб-разработке, где скорость реагирования приложения играет огромную роль. Никто не хочет тратить свое время и ожидать подвисшее приложение, пока вы выполняете несколько запросов к базе, отправляете электронное письмо или запускаете другие потенциально длительные задачи. Пользователи хотят получать ответы на свои действия, и они хотят, чтобы эти ответы происходили мгновенно. Когда ваше приложение становится медленным, вы начнете терять своих клиентов. После того как пользователь сталкивается с подвисанием приложения, в большинстве случаев он или она просто закрывает его и никогда не возвращается. Когда интерфейс зависает, с точки зрения пользователя не ясно — то ли ваше приложение поломано, то ли оно выполняет какие-то длительные задачи и на них требуется некоторое время.
Отзывчивость
Современные приложения, как правило, отзывчивы, но некоторые потенциально долгие задачи или блокирующие операции, такие как сетевые и файловые системы ввода/вывода или запросы к базе могут существенно замедлить работу приложения. Чтобы предотвратить приложения от блокировки с помощью этих операций мы можем запускать их в фоновом режиме, таким образом, скрывая задержки, которые они создают. Итак, приложение остается отзывчивым, т. к. оно может продолжить работу, например, оно может вернуть управление пользовательскому интерфейсу или ответить на другие события.
Параллельность vs асинхронность
Большинство людей, когда они видят асинхронный код, сразу думают “О, это круто! Я могу запустить мои задачи параллельно!”. Я, может вас разочарую, но на самом деле, это неправда, конкурентность (асинхронность) и параллельность — это не то же самое. Это часто встречающаяся ошибка, поэтому давайте попробуем разобраться почему.
Когда что-то выполняется асинхронно, это означает не блокирующее выполнение без ожидания завершения. В то время как параллельность означает выполнение нескольких отдельных задач в одно и то же время, как независимые части.
Сделайте задачу сами в какое-либо свободное время и дайте мне знать, когда закончите, и принесите мне результаты. В это время я могу продолжить выполнение своей задачи.
Асинхронный код требует обработки зависимостей между последовательностью выполнения и делается это с помощью обратных вызовов. Когда какое-то задание выполнилось, оно уведомляет другое задание о том, что оно завершилось. Асинхронный код в основном имеет дело со временем (последовательностью событий).
Наймите столько людей, сколько вы хотите и разделите задачу между ними, чтобы выполнить её быстрее и сообщите мне, когда закончите. Я могу продолжить заниматься своими задачами или, если задача срочная, я останусь здесь и подожду, пока вы вернетесь с результатами. После я могу объединить результаты этих ребят. Параллельное выполнение зачастую требует больше ресурсов, т. е. оно в основном зависит от железа.
Чтобы проиллюстрировать разницу между асинхронным и параллельным выполнением на реальных примерах, мы можем сравнить два популярных веб-сервера: Apache и Nginx. Они прекрасно иллюстрируют эту разницу: Nginx является асинхронным и основан на событиях, в то время как Apache использует параллельные потоки. Apache создает новые потоки для каждого дополнительного подключения, поэтому существует максимально допустимое количество соединений в зависимости от доступной памяти в системе. При достижении этого лимита подключений, Apache отказывается от дополнительных соединений. Ограничивающим фактором в настройке Apache является память (помните, что параллельное выполнение часто зависит от аппаратного обеспечения). Если поток останавливается, клиент ждет ответа, пока поток освободится и вернёт ответ.
Nginx работает иначе, чем Apache и не создаёт новые потоки для каждого входящего запроса. У него есть основной процесс-воркер (или несколько процессов, зачастую на практике рекомендуется иметь по одному воркеру на каждый процессор(CPU)), который является однопоточным. Этот воркер может обрабатывать тысячи одновременных подключений. Он делает это асинхронно с одним потоком, а не с помощью многопоточного параллельного выполнения.
Итак, асинхронность (или конкурентность) — это способ выполнения задач. Она представляет собой композицию из независимо выполняющихся задач. Параллельность — это одновременное выполнение нескольких задач (они могут быть связаны, а могут и нет). В асинхронности, мы имеем дело со многими разными задачами одновременно. Параллельность делает много задач сразу. Звучит одинаково, но в основе разные идеи. Конкурентность — это о структуре, в то время как параллельность — об исполнении.
Асинхронность: у вас есть бекенд задача, которую вы делаете в данный момент. Тут к вам обратился фронтенд разработчик с просьбой немого поправить API. Вы сохранились у себя в ветке, переключились на другую (в данный момент вы заняты задачей с API), поправили и вернулись к своей задаче. После вас позвали на совещание по какой-то тематике, вы прервались, сходили (в данный момент вы выполняете третью задачу, которая будет продолжена на следующем совещании) и вернулись опять к своей первой задаче. Эти три задачи вы выполняли асинхронно — другими словами, прерываясь и переключаясь между задачами, выделяя на них понемногу времени.
Параллельность: если говорить о параллельности в лице одного человека, то в голову приходят пара очевидных примеров. Например, играть на гитаре и петь. В этот момент одна ваша рука ставит аккорды, другая выполняет перебор (или бой) и плюс вы ещё поёте. В этот момент вы параллельно выполняете три задачи. Или другой пример: вы за обедом слушаете Моцарта. Тут вы параллельно выполняете две задачи — едите и слушаете. Но если вернуться к задачам разработки, то получится более наглядный пример. Представьте, что вы работаете в команде из 4 разработчиков. Ваша команда — это эдакая единая машина с четырехъядерным процессором. Каждый разработчик — одно ядро. Когда начинается спринт, каждый из 4 разработчиков выполняет свои задачи параллельно с остальными тремя разработчиками, а в конце спринта вы собираете это воедино.
Зачем на бекенде?
Теперь вы можете возмутиться на то, что на беке вас не особо волнует отзывчивость. У вас на фронте есть все эти гадкие вещи типа асинхронного JavaScript-а, а всё, что должен делать ваш сервер, — это просто отвечать на запросы. Так что обеспечить отзывчивость пользователю — это задача фронта, а не ваша. Да, это правда, но бекенд не ограничивается только API-ответами. Иногда вам нужно управлять какими-то сложными задачами, например, сервер для загрузки видео. В данном случае, возможно, отклик не является ключевым фактором, но мы упираемся в нехватку ресурсов, потому что приложение должно ждать. Он может ждать операций файловой системы, сетевого подключения, запросов к базе и так далее. Часто эти операции ввода/вывода осуществляются крайне медленно по сравнению с расчетами на CPU, например, когда мы конвертируем видеофайлы. А пока мы медленно сохраняем или читаем файл, наш процессор должен ждать и ничего не делать, вместо того, чтобы делать какую-то полезную работу. Как мы уже говорили, вместо ожидания, мы можем выполнять эти задачи в фоновом режиме. Как? Читайте ниже.
Асинхронный РНР
При выполнении этого кода мы видим следующее:
Функция setTimeout() ставит в очередь код, который будет выполняться после завершения текущего стека вызовов. Это означает, что мы нарушаем синхронный поток кода и откладываем выполнение некоторой части кода. Второй вызов console.log() будет выполнен до первого (внутри функции settimeout() ), который был поставлен в очередь.
Но как на счёт PHP? Ну, в PHP из коробки у нас нет хороших и удобных инструментов для написания действительно асинхронного кода. Нет функции эквивалентной settimeout() и мы просто не можем отложить или поставить в очередь какой-то код. Вот почему такие фреймворки и библиотеки, как Amp и ReactPHP, начали появляться. Их основная идея в том, чтобы скрыть от нас низкоуровневые тонкости языка и предоставить инструменты и абстракции высокого уровня, которые могут быть использованы для написания асинхронного кода и управления конкурентностью, как мы могли бы сделать это в JavaScript и NodeJS.
Почему я должен использовать PHP, если у нас есть NodeJs и Go?
Подобный вопрос возникает чаще всего, когда речь идет об асинхронном РНР. Почему-то сообщество часто против использования PHP в качестве инструмента для написания асинхронного кода. Всегда кто-то предлагает просто использовать Go и NodeJs.
Этот твит assertchris прекрасно описывает это:
Basically how I feel about every «just use another language instead of async in PHP» post. pic.twitter.com/LnKXTIQodx
Конечно, когда PHP был создан его целью было не быть языком программирования, который может быть использован для создания больших сложных приложений. Когда это произошло, не было никакого JavaScript и никакого асинхронного кода. Но сейчас PHP совершенно другой, который уже имеет некоторую встроенную функциональность для написания асинхронного кода (например, функцию stream_select() ).
Да, вы можете использовать Go или NodeJs для создания асинхронных приложений, но это не всегда уместно. Когда у вас уже есть солидный опыт в PHP, для вас будет намного проще разобраться в некоторых библиотеках и инструментах, подходящих вам, вместо того, чтобы учить новый язык и новую экосистему. Такие инструменты, как ReactPHP или Amp позволяют писать асинхронный код, как вы пишете это в NodeJS. Эти инструменты являются довольно зрелыми и имеют стабильные версии, так что вы можете смело использовать их в продакшене.
Заключение
Не бойтесь изучать новые парадигмы языка. PHP — это гораздо больше, чем запустить скрипт, выполнить некоторый код и умереть. Вы будете удивлены, используя ваш знакомый и привычный PHP в совершенно новом ключе, в таком, в каком вы никогда не использовали его! Асинхронный код и событийно-ориентированное программирование позволят расширить ваши представления о PHP и о том, как этот язык может быть использован. Нет необходимости изучать новый язык для написания асинхронных приложений только потому, что кто-то винит PHP в том, что это неправильный инструмент для этого, или потому, что я всегда делал это так, и это невозможно улучшить. Просто попробуйте!
Aсинхронный PHP
Десять лет назад у нас был классический LAMP-стек: Linux, Apache, MySQL, и PHP, который работал в медленном режиме mod_php. Мир менялся, а с ним и важность скорости. Появился PHP-FPM, который позволил значительно увеличить производительность решений на PHP, а не срочно переписывать на чем-то побыстрее.
Параллельно велась разработка библиотеки ReactPHP с применением концепции Event Loop для обработки сигналов от ОС и представления результатов для асинхронных операций. Развитие идеи ReactPHP — AMPHP. Эта библиотека использует тот же Event Loop, но поддерживает корутины, в отличие от ReactPHP. Они позволяют писать асинхронный код, который выглядит как синхронный. Возможно, это самый актуальный фреймворк для разработки асинхронных приложений на PHP.
Но скорости требуется всё больше и больше, инструментов уже не хватает, поэтому идея асинхронного программирования в PHP — одна из возможностей ускорить обработку запросов и лучше утилизировать ресурсы.
Об этом и поговорит Антон Шабовта (zloyusr) — разработчик в компании Onliner. Опыт больше 10 лет: начинал с десктопных приложений на С/С++, а потом перешел в веб-разработку на PHP. «Домашние» проекты пишет на C# и Python 3, а в PHP экспериментирует с DDD, CQRS, Event Sourcing, Async Multitasking.
Статья основана на расшифровке доклада Антона на PHP Russia 2019. В ней мы разберемся в блокирующих и неблокирующих операциях в PHP, изучим изнутри структуру Event Loop и асинхронных примитивов, таких как Promise и корутины. Напоследок, узнаем, что нас ждет в ext-async, AMPHP 3 и PHP 8.
Асинхронность — это способность программной системы не блокировать основной поток выполнения.
Асинхронная операция — это операция, которая не блокирует поток выполнения программы до своего завершения.
Вроде бы несложно, но сначала надо понять, какие операции блокируют поток выполнения.
Блокирующие операции
PHP это язык-интерпретатор. Он читает код построчно, переводит в свои инструкции и выполняет. На какой строке из примера ниже код заблокируется?
Это происходит потому, что PHP не знает, как долго SQL-сервер будет обрабатывать этот запрос, и выполнится ли он вообще. Он ждет ответа от сервера и все это время программа не выполняется.
Также PHP блокирует поток выполнения на всех I/O операциях.
Асинхронный SQL-клиент
Но современный PHP — это язык общего назначения, а не только для веба как PHP/FI в 1997 году. Поэтому мы можем написать асинхронный SQL-клиент с нуля. Задача не самая тривиальная, но решаемая.
Что делает такой клиент? Подключается к нашему SQL-серверу, переводит работу сокета в неблокирующий режим, пакует запрос в бинарный формат понятный SQL-серверу, записывает данные в сокет.
Так как сокет в неблокирующем режиме, то операция записи со стороны PHP выполняется быстро.
Но что вернется как результат такой операции? Мы не знаем, что ответит SQL-сервер. Он может долго выполнять запрос или не выполнить вообще. Но что-то же надо вернуть? Если мы используем PDO и вызываем update запроса на SQL-сервере, нам возвращается affected rows — количество строк измененных этим запросом. Это мы вернуть пока не можем, поэтому только обещаем возврат.
Promise
Promise — это объект-обертка над результатом асинхронной операции. При этом результат операции нам пока неизвестен.
К сожалению, нет единого стандарта Promise, а перенести стандарты из мира JavaScript в PHP напрямую не получается.
Как работает Promise
Если произойдет ошибка, то выполнится колбэк onReject для обработки ошибки.
Интерфейс Promise выглядит примерно так.
У Promise есть статус и методы для установки колбэков и заполнения ( resolve ) Promise данными или ошибкой ( reject ). Но есть отличия и вариации. Методы могут называться иначе, либо вместо отдельных методов для установления колбэков, resolve и reject может быть какой-то один, как в AMPHP, например.
Часто методы для заполнения Promise resolve и reject выносят в отдельный объект Deferred — хранилище состояния асинхронной функции. Его можно рассматривать, как некую фабрику для Promise. Он одноразовый: из одного Deferred получается один Promise.
Как это применить в SQL-клиенте, если мы решим писать его сами?
Асинхронный SQL-клиент
Сначала мы создали Deferred, выполнили всю работу сокетами, записали данные и вернули Promise — все просто.
Когда у нас есть Promise, мы можем, например:
Event Loop
Существует концепция Event Loop — цикл событий. Он умеет обрабатывать сообщения в асинхронной среде. Для асинхронного I/O это будут сообщения от ОС о том, что сокет готов к чтению или записи.
Выразим эту концепцию в коде: возьмем простейший случай, уберем обработку ошибок и другие нюансы, чтобы остался один бесконечный цикл. В каждой итерации он будет опрашивать ОС о сокетах, которые готовы к чтению или записи, и вызывать колбэк для конкретного сокета.
Дополним наш SQL-клиент. Мы сообщаем Event Loop, что как только в сокет, с которым мы работаем, придут данные от SQL-сервера, нам надо привести Deferred в состояние «выполнено» и передать данные из сокета в Promise.
Event Loop умеет обрабатывать наши I/O и работает с сокетами. Что еще он может делать?
Реализации Event Loop
Написать свой Event Loop не только можно, но и нужно. Если хотите работать с асинхронным PHP, важно написать свою простую реализацию, чтобы понять, как это работает. Но в продакшн мы это, естественно, использовать не будем, а возьмем готовые реализации: стабильные, без ошибок и проверенные в работе.
Существует три основных реализации.
ReactPHP. Самый старый проект, начинался еще с PHP 5.3. Сейчас минимальная требуемая версия PHP 5.3.8. Проект реализует стандарт Promises/A из мира JavaScript.
AMPHP. Именно эту реализацию я предпочитаю использовать. Минимальное требование PHP 7.0, а со следующей версии уже 7.3. Здесь используются корутины поверх Promise.
Swoole. Это интересный китайский фреймворк, в котором разработчики пытаются перенести в PHP некоторые концепции из мира Go. На английском документация неполная, большая часть на GitHub на китайском. Если знаете язык — вперед, но мне пока работать страшно.
ReactPHP
Посмотрим как будет выглядеть клиент с использованием ReactPHP для MySQL.
Все почти также, как мы написали: создаем Сonnection и выполняем запрос. Можем установить колбэк для обработки результатов (вернуть affected rows ):
и колбэк для обработки ошибок:
Из этих колбэков можно строить длинные-длинные цепочки, потому что каждый результат then в ReactPHP также возвращает Promise.
Это решение проблемы, которая называется «callback hell». К сожалению, в реализации ReactPHP это приводит к проблеме «Promise hell», когда для корректного подключения RabbitMQ, требуется10-11 колбэков. Работать с таким кодом и исправлять его сложно. Я быстро понял, что это не мое и перешел на AMPHP.
AMPHP
Этот проект младше, чем ReactPHP, и продвигает иную концепцию — корутины. Если посмотреть на работу с MySQL в AMPHP, то видно, что это почти аналогично работе с PDOConnection в PHP.
Генераторы
Ключевое слово yield превращает нашу функцию в генератор.
Генераторы наследуют интерфейс итератора.
Корутины
Но у генератора есть функция интереснее — мы можем снаружи отправить данные в генератор. В этом случае это уже не совсем генератор, а корутина или сопрограмма.
Кроме отправки данных в генератор можно отправлять ошибки и обрабатывать их изнутри, что удобно.
Подытожим. Корутина это компонент программы, который поддерживает остановку и продолжение выполнения с сохранением текущего состояния. Корутина помнит свой стек вызовов, данные внутри, и может их использовать в дальнейшем.
Генераторы и Promise
Посмотрим на интерфейсы генераторов и Promise.
Они выглядят одинаково, за исключением разных названий методов. Мы можем отправить данные и выкинуть ошибку и в генератор, и в Promise.
Как это можно использовать? Напишем функцию.
То же можно провернуть и с ошибками. Если Promise завершился с ошибкой, например, SQL-сервер сказал: «Too many connections», то можем выкинуть ошибку внутрь генератора и перейти на следующий шаг.
Все это подводит нас к важному понятию кооперативной многозадачности.
Кооперативная многозадачность
Это тип многозадачности, при котором следующая задача выполняется только после того, как текущая задача явно объявит себя готовой отдать процессорное время другим задачам.
Я редко встречаюсь с чем-то простым как, например, работа только с одной БД. Чаще всего в процессе обновления пользователя надо обновить данные в БД, в поисковом индексе, потом почистить или обновить кэш, а после еще отправить 15 сообщений в RabbitMQ. В PHP это все выглядит так.
Мы выполняем операции одну за одной: обновили базу, индекс, потом кэш. Но по умолчанию PHP блокирует на таких операциях (I/O), поэтому, если приглядеться, на самом деле все так.
На темных частях мы заблокировались. Они занимают больше всего времени.
Если мы работаем в асинхронном режиме, то этих частей нет, таймлайн выполнения прерывистый.
Можно все это склеить и выполнять кусочки один за одним.
Для чего все это? Если посмотреть на размер таймлайна, то сначала он занимает много времени, но как только мы склеиваем — приложение ускоряется.
Сама концепция Event Loop и кооперативной многозадачности давно применяется в различных приложениях: Nginx, Node.js, Memcached, Redis. Все они используют внутри Event Loop и построены на этом же принципе.
Раз уж мы начали говорить о веб-серверах Nginx и Node.js, давайте вспомним, как происходит обработка запросов в PHP.
Обработка запроса в PHP
Браузер отправляет запрос, он попадает на HTTP-сервер за которым стоит пул FPM-потоков. Один из потоков берет в работу этот запрос, подключает наш код и начинает его выполнять.
Когда приходит следующий запрос, другой FPM-поток его заберет, подключит код и он будет выполняться.
В этой схеме работы есть плюсы.
Асинхронный HTTP-сервер
Это можно сделать. Мы уже научились работать с сокетами в неблокирующем режиме, а HTTP-соединение это такой же сокет. Как он будет выглядеть и работать?
Это пример старта HTTP-серверов в фреймворке AMPHP.
Все достаточно просто: загружаем Application и создаем пул сокетов (один или несколько).
В ReactPHP это будет выглядеть приблизительно также, но только там будет 150 колбэков на разные варианты, что не очень удобно.
Проблемы
С асинхронностью в PHP есть несколько проблем.
Отсутствие стандартов. Каждый фреймворк: Swoole, ReactPHP или AMPHP, реализует свой интерфейс Promise, и они несовместимы.
AMPHP теоретически может взаимодействовать с Promise от ReactPHP, но есть нюанс. Если код для ReactPHP написан не очень грамотно, и где-то неявно вызывает или создает Event Loop, то получится так, что внутри будут крутиться два Event Loop.
Есть относительно хороший стандарт Promises/A+ у JavaScript, который реализует Guzzle. Было бы хорошо, если фреймворки будут ему следовать. Но пока этого нет.
Блокирующие операции. Жизненно важно не блокировать Event Loop когда мы пишем асинхронный код. Приложение замедляется как только мы блокируем поток выполнения, каждая из наших корутин начинает работать медленней.
Найти такие операции для AMPHP поможет пакет kelunik/loop-block. Он выставляет таймер на очень маленький интервал. Если таймер не срабатывает, значит мы где-то заблокировались. Пакет помогает в поиске блокирующих мест, но не всегда: блокировки в некоторых расширениях может не заметить.
Указание типа. Это проблема AMPHP и корутин.
PHP 8
Что нас ждет в PHP 8? Расскажу о своих предположениях или, скорее, желаниях (прим. ред.: Дмитрий Стогов знает, что на самом деле появится в PHP 8).
Generics. В Go ждут Generics, мы ждем Generics, все ждут Generics.
Но мы ждем Generics не для коллекций, а чтобы указать, что результатом выполнения Promise будет именно объект User.
Зачем все это?
Ради скорости и производительности.
PHP — это язык, в котором большая часть операций это I/O bound. Мы редко пишем код, который значительно завязан на вычислениях в процессоре. Скорее всего, у нас это работа с сокетами: надо сделать запрос в базу, что-то прочитать, вернуть ответ, отправить файл. Асинхронность позволяет ускорить такой код. Если посмотреть среднее время ответов на 1000 запросов, мы можем ускориться примерно в 8 раз, а на 10 000 запросов почти в 6!
13 мая 2020 года мы во второй раз соберемся на PHP Russia, чтобы обсудить язык, библиотеки и фреймворки, способы увеличения производительности и подводные камни хайповых решений. Мы приняли первых 4 доклада, но Call for Papers еще идет. Подавайте заявки, если хотите поделиться с сообществом своим опытом.