php curl асинхронный запрос

ez code

Продвинутое использование cURL в PHP

cURL — это инструмент, позволяющий взаимодействовать с различными серверами и поддерживающий множество протоколов: HTTP, FTP, TELNET и др. Изначально cURL — это служебная программа для командной строки. Но, к счастью для нас, PHP поддерживает работу с библиотекой cURL. В этой статье мы рассмотрим нетривиальные примеры работы с cURL.

Почему cURL?

На самом деле, есть много других способов отправить запрос на другой сервер чтобы, например, получить содержимое страницы. Многие, в основном из-за лени, используют простые PHP функции, вместо cURL:

Однако они не позволяют эффективно обрабатывать ошибки. Также есть ряд задач, которые им вовсе не под силу — например, работа с cookies, авторизация, post запросы, загрузка файлов.

cUrl — мощный инструмент, который поддерживает множество протоколов и предоставляет полную информацию о запросе.

Основы cUrl

Прежде чем перейти к сложным примерам, рассмотрим базовую структуру cURL запроса в PHP. Для выполнения cURL запроса в PHP необходимо сделать 4 основных шага:

В основном в этой статье мы будем рассматривать шаг №2, так как там происходит основная магия. Список cURL опций очень большой, поэтому все опции рассматривать сегодня мы не будем, а используем те, которые пригодятся для решения конкретных задач.

Отслеживание ошибок

При необходимости, вы можете добавить следующие строки для отслеживания ошибок:

Обратите внимание, мы используем «===» вместо «==», т.к. надо отличать пустой ответ сервера от булевского значения FALSE, которое возвращается в случае ошибки.

Получение информации о запросе

Другой необязательный шаг — получение информации о cURL запросе, после его выполнения.

В результате вы получите массив со следующей информацией:

Отслеживание редиректов, в зависимости от браузера

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

Мы будем использовать опцию CURLOPT_HTTPHEADER для установки наших собственных заголовков, включая User-Agent и язык и посмотрим, куда перенаправляют нас сайты.

В цикле проверяем браузеры для каждого урла. Сперва мы устанавливаем опции для нашего запроса: URL и тестируемый браузер и язык.

Т.к. мы установили специальную опцию, результат выполнения запроса будет содержать только HTTP заголовки. С помощью простого регулярного выражения мы можем проверить содержит ли ответ строку «Location:».

Результат выполнения скрипта:

Отправляем POST запросы

При выполнении GET запросов данные можно передавать в строке запроса. Например, когда вы ищете в гугле, ваш запрос передается в URL:

Чтобы получить результат этого запроса, вам даже не понадобится cURL, вы можете быть ленивым и использовать «file_get_contents()».

Но некоторые HTML формы используют метод POST. В таком случае данные отправляются в теле сообщения запроса, а не в самом URL.

Напишем скрипт, который будет отправлять POST запросы. Для начала создадим простой PHP файл, который будет принимать эти запросы и возвращать отправленные ему данные. Назовем его post_output.php :

Далее напишем PHP скрипт, который отправит cURL запрос:

Данный скрипт выведет:

Загрузка файлов

Загрузка файлов очень похожа на предыдущий скрипт, т.к. загрузка файлов всегда выполняется с помощью POST запросов.

Так же как и в предыдущем примере, создадим файл, который будет принимать запросы, upload_output.php :

И сам скрипт, загружающий файлы:

Если вы хотите загрузить файл, все что необходимо — это передать путь к нему, так же как обычный параметр POST запроса, поставив вначале «@». Результат работы скрипта:

Multi cURL

Одна из продвинутых возможностей cURL в PHP — это возможность выполнения нескольких запросов одновременно и асинхронно.

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

Идея состоит в том, что вы можете создать множество cURL дескрипторов, объединить их под одним мульти-дескриптором и выполнять их асинхронно.

В данном примере мы просто выводим результат запросов в STDOUT. Рассмотрим нетривиальный случай применения multi cURL.

Проверка внешних ссылок в WordPress

Представьте себе блог с большим количеством постов, содержащих ссылки на внешние сайты. Некоторые из этих ссылок могут быть не рабочими.

Напишем скрипт, который найдет все нерабочие ссылки и покажет их нам.

Для начала нам необходимо вытащить все внешние ссылки из базы данных:

В этой части скрипта мы просто вытаскиваем из базы все внешние ссылки. Проверим их:

Рассмотрим код подробнее (нумерация соответствует комментариям в коде):

Другие возможности cURL в PHP

HTTP аутентификация

Если HTTP запрос требует аутентификацию, используйте следующий код:

Загрузка по FTP

В PHP есть своя библиотека для работы с FTP, но можно использовать и cURL:

Использование прокси

Запросы можно выполнять через определенный proxy:

Колбэки (callback functions)

Есть возможность использовать колбэки во время выполнения запроса, не дожидаясь его завершения. Например, во время того как ответ сервера загружается мы можем использовать уже полученные данные, не дожидаясь полной загрузки.

Колбэк функция должна возвращать длину строки для правильной работы запроса.

Каждый раз, когда будет получена очередная часть ответа сервера, будет вызван колбэк.

Заключение

В этой статье мы рассмотрели продвинутые возможности cURL в PHP. В следующий раз, когда вам понадобится делать URL запросы — используйте cURL.

Источник

Асинхронное параллельное исполнение в PHP

Много пик сломано в мире на тему того, можно ли и как создавать многопоточность в PHP. Чаще всего все сводится к тому, что так делать нельзя или дискуссия материализуется в какие-то ужасные костыли (ох, сколько я их уже повидал). Я хочу изложить свою точку зрения на этот вопрос. Легко догадаться, что если бы моя позиция была “так нельзя” или “это зло”, то я бы не писал эту статью. Вот только погодите, не спешите доставать тухлые яйца и вооружаться мелкими бытовыми предметами для рукопашной схватки. Я постараюсь дипломатично изложить тему и максимально объективно раскрыть ситуацию. Так что самые смелые из моих читателей могут прочитать молитву от ереси и открыть статью.

php curl асинхронный запрос. Смотреть фото php curl асинхронный запрос. Смотреть картинку php curl асинхронный запрос. Картинка про php curl асинхронный запрос. Фото php curl асинхронный запрос

Объект дискуссии – что я подразумеваю под многозадачностью/многопоточностью?

Существует много разных терминов: multithreading, multiprocess, asynchronous execution. Они все означают разные вещи. Однако, часто бывает так, что на практике нам, как потребителям, не так уж и важно, в разных процессах, потоках или еще как параллельно исполняется наша программа. Лишь бы она работала быстрее и не теряла отзывчивость в процессе своего выполнения. Поэтому в этой статье я рассмотрю все возможные варианты параллелизации PHP вне зависимости от внутренней кухни этой самой параллелизации. То есть, я попытаюсь ответить на вопрос: как можно сделать так, чтобы какое-то долгое действие в моем PHP коде выполнялось в фоне, пока мой код занят чем-то другим полезным.

А зачем оно вообще надо?

Вообще-то, я считаю, что в 99% случаев оно не надо (и заметьте, это пишет автор статьи на тему параллелизации). Я проработал 8 лет с PHP и до прошлой недели всегда считал большой глупостью пытаться вкрутить многопоточность в PHP. Дело в том, что задача PHP – это принять входящий HTTP запрос и сгенерировать на него ответ. Один запрос – один ответ. Схема весьма простая, и очень удобно вести обработку линейно в одном потоке. Мне кажется, что в связке клиент-сервер на сервере делать что-то многопоточным не надо, за исключением каких-то особенных обстоятельств, которые вас вынуждают к этому и на которых можно сыграть для уменьшения потребляемых ресурсов и времени ответа. Почему я так считаю? Ведь кто-то может сказать, что если распараллелить какой-то процесс, то он может выполняться на 2х ядрах сразу и таким образом выполнится быстрее. Это правда. Но на сервере всегда есть один нюанс: вы должны быть готовы обрабатывать сразу N клиентов одновременно. И если ваш серверный код “расползется” на все 8 доступных ядер, и в этот момент придет новый входящий запрос, то ему придется ютиться в очереди, ожидая, пока какое-то ядро будет готово начать его обработку. И у вас начнется бесполезная конкуренция за циклы 8-ми CPUs между 16 потоками/процессами. Именно поэтому я считаю, что даже если на сервере есть ресурсы, которые можно привлечь в обработку входящего запроса за счет параллелизации, лучше так не делать. Можно сказать, что параллелизация уже присутствует и так, т.к. сервер может одновременно обрабатывать несколько входящих запросов. Ну и получается, что вкручивать параллелизацию в параллелизацию – это уже как бы перебор.

Я к этому моменту уже мельком упомянул, зачем нужна многопоточность. Она позволяет “разрывать” единую нить исполнения кода. Из этого следует сразу несколько “полезных” следствий. Во-первых, можно в фоне выполнять какое-то медленное действие, сохраняя отзывчивость основного потока программы (асинхронное исполнение). К примеру: нам по бизнес логике нужно обменяться информацией с каким-нибудь периферийным устройством. Это устройство очень медленное, и операция занимает около 5 секунд. Если все это делать в одном потоке, то блок-схема нашего алгоритма выглядит вот так:
php curl асинхронный запрос. Смотреть фото php curl асинхронный запрос. Смотреть картинку php curl асинхронный запрос. Картинка про php curl асинхронный запрос. Фото php curl асинхронный запрос

Очень вероятно, что хоть какую-то часть из “разрушение системы и подчистка за собой” можно выполнять не зная ответа от периферийного устройства. Тогда можно вызвать периферийное устройство в отдельном потоке, и пока устройство отвечает, никто нам не запретит в основном потоке выполнить что-то из пункта “разрушение системы и подчистка”. Тогда блок-схема выглядит вот так:
php curl асинхронный запрос. Смотреть фото php curl асинхронный запрос. Смотреть картинку php curl асинхронный запрос. Картинка про php curl асинхронный запрос. Фото php curl асинхронный запрос

Ну а второе большое следствие из разрыва этой единой нити исполнения это то, что можно использовать больше ресурсов компьютера, на котором исполняется код. Если это какое-то трудоемкое математическое вычисление, то его можно запустить на несколько потоков (при условии, что алгоритм вычислений это позволяет), и тогда над результатом будут сразу работать несколько ядер процессора. То бишь, в абстрактной формулировке: если в несколько потоков конкурировать за какой-то ресурс, то его можно получить больше за тот же промежуток времени. Хотя, я уже говорил, что этот пункт весьма сомнителен для меня в случае серверного кода (а мы именно такой и рассматриваем).

Недостатки и трудности параллелизации

Самый большой, да и единственный значимый, мне кажется – это дополнительные расходы на поддержание многоточности, как при run time (потокам нужно как-то обмениваться между собой информацией, чтобы они могли работать на общее благо), так и в development time (многопоточные/асинхронные программы сложнее писать и поддерживать, т.к. человеческому мозгу куда проще воспринимать линейную логику исполнения). Ведь мы всегда воспринимаем мир “здесь и сейчас”, а осознать, что где-то еще происходит что-то еще, всегда получается не так ярко как текущую сцену “здесь и сейчас”. Получается, что многопоточная программа будет потреблять больше ресурсов, выполняя ту же самую задачу, что и однопоточная программа. Также, очень вероятно, она будет требовать больше времени на разработку и поддержание/расширение. Что же получается? Многопоточность может дать прирост в скорости работы за счет параллельного исполнения, и многоточность будет потреблять больше ресурсов из-за необходимости синхронизации и обмена данными между потоками. В общем-то, как всегда: параллелизация – это всего лишь инструмент. При правильном использовании он может оказаться полезным, в противном случае он будет лишним грузом.

Что меня заставило реализовать многопоточность в PHP

Я работаю над вебсайтом, который оценивает себестоимость отправки посылки из пункта А в пункт Б. По сути, это агрегатор API грузоперевозчиков. Ну, вы уже догадались? При холодном кеше в худших случаях могло получиться так, что из 1 входящего запроса на мой вебсайт “вырастают” под 200 запросов на API разных грузоперевозчиков. Естественно, делать 200 синхронных HTTP запросов – это грех куда страшнее, чем вкручивание многопоточности в PHP. Поэтому из 2-х зол я выбрал меньшее. У меня действительно был случай один на миллион в этой ситуации. Дополнительные потоки не потребляют много циклов CPU, ни сети, ни жесткого диска, очень условно говоря, мне нужны были потоки, в которых всего лишь нужно было поспать 1-2 секунды (пока генерируется ответ на стороне API перевозчика). Disclaimer: 10 раз подумайте, прежде реализовывать это у себя где-то в проекте. Я за 8 лет работы первый раз встретил случай, где это имеет смысл. Одевайте этот хомут на шею только если он вам действительно нужен.

Эволюция моей мысли (моего алгоритма)

Прежде чем я перейду к конкретным способам реализации, которые я пробовал, позвольте представить некоторые теоретические размышления на эту тему. Самое крупное деление алгоритмов разбивается по линии fork/thread текущего PHP процесса или запуск нового дочернего процесса. Во-первых, я нашел только 1 возможную реализацию на forking текущего PHP процесса (которая оказалась нерабочей ко всему прочему). Во-вторых, fork’аться в PHP – это явно пример плохой параллелизации. Почему? Да потому, что в параллельный поток нужно стараться вынести исключительно то действие, которое выполняется медленно (связь с периферийным устройством, а не интерпретацию его ответа; выполнение HTTP запроса, а не его интерпретация опять же). Смотрите пункт про параллелизацию серверного кода вверху, чтобы понять почему нужно делать именно так. В-третьих, fork’ая полноценную программу, вы обязуете себя на синхронизацию данных между ними, а это очень непросто. С другой стороны, если вы создаете дочерний процесс с буквально парочкой строк кода, которые выполняют какую-то муторную и долго-спящую операцию, вам ничего не надо синхронизировать и вы не “расползаетесь” на остальные ядра вашего сервера. Ну и как бонус, этот подход в разы гибче – вы можете запустить не PHP дочерний процесс, а bash скрипт какой-нибудь или программу на C, или вообще что попало, тут вас ограничивает только ваша фантазия (и знания Линукса, конечно).

Через curl_multi_exec()

Как по мне, самое бредовое из того, что я видел про параллелизацию PHP – это запускать curl_multi_exec() на свой же вебсайт. Если вы хотите что-то исполнять асинхронно на локальной ОС, зачем вам вообще нужно подключать под это дело HTTP стек? Только потому, что cURL умеет делать асинхронные запросы? Аргумент слабоват. Реализовывать таким способом асинхронность относительно удобно, но у вас будет лишняя нагрузка на ваш вебсервер, и вам его будет сложнее настроить оптимально в плане max child worker processes (в Apache prefork MPM это “MaxClients”, в PHP FPM это “pm.max_children”), т.к. очень вероятно, что существует огромная разница между валидными входящими запросами и вашими внутренними запросами, вырожденными из асинхронности. Ну и о вопросах синхронизации данных между подпроцессами можно забыть. Ваш максимум коммуникации – это HTTP запрос и HTTP ответ. Можно общаться еще через БД, но я боюсь представить, что вам придется делать, если вы используете транзакции и вам нужно одну и ту же транзакцию видеть из обоих обработчиков.

Другое дело, если запросы отправляются на другой сервер, как в моем случае. Здесь использование curl_multi_exec() я считаю оправданным. Ваш главный плюс: можно завестись с полпинка, работы много делать не надо. Ваши минусы: не каждую логику программы можно так “вывернуть”, чтобы можно было запустить несколько cURL запросов из одного места и в том же самом месте обработать их результаты. К примеру, запросы могут идти на 2 разных хоста, и каждый из хостов может отвечать в своем собственном формате. Таким образом ваш код запросто может стать неудобным в поддержке вокруг этого curl_multi_exec() вызова. Второй минус заключается в том, что у вас мало гибкости. В этом подходе вы ничего кроме HTTP запросов асинхронно сделать никогда не сможете.

Пример (сразу скажу, я его на деле сильно не пробовал, т.к. негибкость этого подхода меня сильно отпугивала): php.net/manual/en/function.curl-multi-exec.php

Через pcntl_fork()

Якобы, pcntl_fork() можно использовать, когда PHP работает из CLI. По поводу CGI и FastCGI интерфейсов я не уверен. Т.к. я был ограничен во времени и не занимался исследованием вакуумного пространства в полнолуние високосного года, а решал конкретную задачу, то я больше не уделил внимания этому варианту. Как я понимаю, если pcntl_fork() можно запускать из FastCGI интерфейса, то этот вариант должен работать при связке Nginx + PHP-FPM (Apache тоже можно вместо Nginx в этой формуле, просто кто использует Apache с PHP-FPM?). Может быть у кого-то из читателей есть больше опыта в этом вопросе? Пишите комментарии, и я расширю этот раздел согласно вашим дополнениям. Могу предположить, что вас ожидает достаточно проблем связанных с file descriptors и коннектами к БД, если вы пойдете путем fork’ания.

Через exec()

Самый скорый на реализацию метод, который позволяет запускать любую команду в ОС (читайте “очень гибкий”). До столкновения с моей текущей задачей я всего лишь раз прибегал к этой функции. Но у меня там случай был другой, мне просто нужно было запустить команду, дать ей входные аргументы и забыть о ней. Никаких ответов мне от нее не нужно было. В процессе выполнения команда сама куда надо запишет результат. Тогда я это реализовал вот так:

Без амперсанда в конце команды, она бы выполнялась синхронно, т.е. PHP процесс бы спал до окончания работы команды. Говоря по правде, я на этом способе тоже сильно долго не задерживался, но по идее, все необходимые входные аргументы можно передать через аргументы команды. STDERR и STDOUT команды можно перенаправить в файлы или named pipes. Если очень надо, то можно предварительно записать STDIN в файл или named pipe. В самом худшем случае получится где-то вот такой франкенштейн:

Самый большой недостаток, заключается в том, что нет хорошего способа определить закончила ли свою работу команда. Но если изначально заложить определенный интерфейс содержимого STDOUT, то можно жить весьма неплохо и на этом способе. Вам тогда нужно периодически считывать содержимое STDOUT, и дальше, основываясь на вашем протоколе содержимого STDOUT, можно интерпретировать в каком состоянии находится команда. Под передачу мета информации о текущем статусе можно завести отдельный файл дескриптор (4й, допустим).

Через popen()

Итак, наш следующий кандидат выглядит немного выигрышнее, чем exec(«my-command &») вариант. Вы уже заметили, что я пытаюсь их строить по возрастанию? Этот способ позволяет запускать команду и возвращает file descriptor на pipe. В зависимости от 2го аргумента функции popen() это будет либо STDIN дочерней команды, либо STDOUT дочерней команды. Получается как-то однобоко… Либо глухой, либо немой, зато удобно в 1 строку и с минимальной низкоуровневой морокой. Мне больше всего понравился вот такой вариант:

Якобы, pclose() функция вернет exit code дочерней команды. Однако, php.net предупреждает, что особо доверять этому значению не нужно. Я дополнительных практических заметок по этому поводу не могу сказать, т.к. этот метод практически не тестировал.

Из недостатков: невозможно узнать текущий статус дочерней команды. Иногда это может быть важным. Представьте, что мы запустили 10 асинхронных команд. Они все выполняются где-то 2 секунды, ± 10%. И в основном процессе PHP мы по окончанию каждой из команды хотим проинтерпретировать результаты. Каждая такая интерпретация у нас займет 0.5 секунды. Если бы у нас была возможность узнать закончила ли работу такая-то дочерняя команда, то мы бы могли читать STDOUT тех дочерних команд, которые уже закончили свою работу. И пока мы занимаемся интерпретацией текущей дочерней команды, остальные выполняющиеся дочерние команды весьма вероятно успеют завершиться к моменту, когда мы будем готовы интерпретировать их результат. Увы, мы так делать не можем. Максимум, что мы можем делать в этом способе – это читать STDOUT, и если STDOUT дочерней команды еще открыт, то наш основной PHP процесс будет спать до тех пор, пока дочерняя команда его не закроет либо не закончит свою работу (что неявным образом тоже закроет pipe). Мы немного теряем асинхронность, т.к. в определенных ситуациях можем “напороться” на долгий “сон” основного PHP процесса.

Второй недостаток: односторонность коммуникации. Хорошо бы иметь сразу 3 дескриптора… на все случаи жизни, так сказать. И если без STDIN можно обойтись (можно все входные данные запихнуть во входные аргументы дочерней команды), то вот без STDERR все-таки сложнее. Умельцы могут придумать такое решение:

Через proc_open()

Итого имеем победителя! Он нам подходит по всем критериям и запускается довольно небольшим количеством кода в PHP. Я на практике уже около недели использую этот способ и не имею нареканий. Вебсайт и сервер не видели еще настоящей нагрузки, т.к. вебсайт еще не вышел на финальную стадию, но тьфу-тьфу-тьфу у меня нет причин нарекать, и кажется, я написал код, в возможность существования которого я сам не верил дней 10 назад.

Конкретные трюки и уловки

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

base64_encode(serialize())
Запускайте как можно раньше, потребляйте результаты как можно позже

Умелое использование асинхронного вызова – это большая наука. Нетрудно догадаться, что нужно максимально агрессивно (eager) запускать дочернюю асинхронную команду и максимально лениво (lazy) читать результаты ее работы. Ведь таким образом у вашей команды будет максимум времени на исполнение и ваш основной поток не будет заблокирован в спячке, ожидая окончание ее работы. Не все архитектуры программ удобно ложатся под этот принцип. Вы должны понимать, что запускать дочернюю команду и следующей же строчкой читать ее результат – это делать себе только хуже. Если вы планируете использовать асинхронность, постарайтесь продумать свою архитектуру как можно раньше и всегда держите это в уме. На своем конкретном примере скажу, что когда я дошел до понимания, что мне нужно параллелить свой PHP процесс, мне пришлось переписать основной движок вебсайта, развернув его более удобной стороной к асинхронности.

Прикройте асинхронный вызов синхронным кешем

Асинхронность – это лишние расходы. На синхронизацию данных (если она у вас будет), на создание и убивание дочернего процесса, на открытие и закрытие файл дескрипторов и т.п. На моем ноутбуке асинхронный вызов простого echo «a» занимает около 3 мс. Вроде бы не много, но я вам говорил, что у меня вплоть до 200 асинхронных вызовов может быть. 200 х 3 = 600 мс. Вот я и потерял уже 600 мс в никуда.

Источник

Aсинхронный PHP

Десять лет назад у нас был классический LAMP-стек: Linux, Apache, MySQL, и PHP, который работал в медленном режиме mod_php. Мир менялся, а с ним и важность скорости. Появился PHP-FPM, который позволил значительно увеличить производительность решений на PHP, а не срочно переписывать на чем-то побыстрее.

Параллельно велась разработка библиотеки ReactPHP с применением концепции Event Loop для обработки сигналов от ОС и представления результатов для асинхронных операций. Развитие идеи ReactPHP — AMPHP. Эта библиотека использует тот же Event Loop, но поддерживает корутины, в отличие от ReactPHP. Они позволяют писать асинхронный код, который выглядит как синхронный. Возможно, это самый актуальный фреймворк для разработки асинхронных приложений на PHP.

php curl асинхронный запрос. Смотреть фото php curl асинхронный запрос. Смотреть картинку php curl асинхронный запрос. Картинка про php curl асинхронный запрос. Фото php curl асинхронный запрос

Но скорости требуется всё больше и больше, инструментов уже не хватает, поэтому идея асинхронного программирования в 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

php curl асинхронный запрос. Смотреть фото php curl асинхронный запрос. Смотреть картинку php curl асинхронный запрос. Картинка про php curl асинхронный запрос. Фото php curl асинхронный запрос

php curl асинхронный запрос. Смотреть фото php curl асинхронный запрос. Смотреть картинку php curl асинхронный запрос. Картинка про php curl асинхронный запрос. Фото php curl асинхронный запрос

Если произойдет ошибка, то выполнится колбэк onReject для обработки ошибки.

php curl асинхронный запрос. Смотреть фото php curl асинхронный запрос. Смотреть картинку php curl асинхронный запрос. Картинка про php curl асинхронный запрос. Фото php curl асинхронный запрос

Интерфейс Promise выглядит примерно так.

У Promise есть статус и методы для установки колбэков и заполнения ( resolve ) Promise данными или ошибкой ( reject ). Но есть отличия и вариации. Методы могут называться иначе, либо вместо отдельных методов для установления колбэков, resolve и reject может быть какой-то один, как в AMPHP, например.

Часто методы для заполнения Promise resolve и reject выносят в отдельный объект Deferred — хранилище состояния асинхронной функции. Его можно рассматривать, как некую фабрику для Promise. Он одноразовый: из одного Deferred получается один Promise.

php curl асинхронный запрос. Смотреть фото php curl асинхронный запрос. Смотреть картинку php curl асинхронный запрос. Картинка про php curl асинхронный запрос. Фото php curl асинхронный запрос

Как это применить в 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 на китайском. Если знаете язык — вперед, но мне пока работать страшно.

php curl асинхронный запрос. Смотреть фото php curl асинхронный запрос. Смотреть картинку php curl асинхронный запрос. Картинка про php curl асинхронный запрос. Фото php curl асинхронный запрос

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 curl асинхронный запрос. Смотреть фото php curl асинхронный запрос. Смотреть картинку php curl асинхронный запрос. Картинка про php curl асинхронный запрос. Фото php curl асинхронный запрос

Мы выполняем операции одну за одной: обновили базу, индекс, потом кэш. Но по умолчанию PHP блокирует на таких операциях (I/O), поэтому, если приглядеться, на самом деле все так.

php curl асинхронный запрос. Смотреть фото php curl асинхронный запрос. Смотреть картинку php curl асинхронный запрос. Картинка про php curl асинхронный запрос. Фото php curl асинхронный запрос

На темных частях мы заблокировались. Они занимают больше всего времени.

Если мы работаем в асинхронном режиме, то этих частей нет, таймлайн выполнения прерывистый.

php curl асинхронный запрос. Смотреть фото php curl асинхронный запрос. Смотреть картинку php curl асинхронный запрос. Картинка про php curl асинхронный запрос. Фото php curl асинхронный запрос

Можно все это склеить и выполнять кусочки один за одним.

php curl асинхронный запрос. Смотреть фото php curl асинхронный запрос. Смотреть картинку php curl асинхронный запрос. Картинка про php curl асинхронный запрос. Фото php curl асинхронный запрос

Для чего все это? Если посмотреть на размер таймлайна, то сначала он занимает много времени, но как только мы склеиваем — приложение ускоряется.

Сама концепция Event Loop и кооперативной многозадачности давно применяется в различных приложениях: Nginx, Node.js, Memcached, Redis. Все они используют внутри Event Loop и построены на этом же принципе.

Раз уж мы начали говорить о веб-серверах Nginx и Node.js, давайте вспомним, как происходит обработка запросов в PHP.

Обработка запроса в PHP

Браузер отправляет запрос, он попадает на HTTP-сервер за которым стоит пул FPM-потоков. Один из потоков берет в работу этот запрос, подключает наш код и начинает его выполнять.

php curl асинхронный запрос. Смотреть фото php curl асинхронный запрос. Смотреть картинку php curl асинхронный запрос. Картинка про php curl асинхронный запрос. Фото php curl асинхронный запрос

Когда приходит следующий запрос, другой FPM-поток его заберет, подключит код и он будет выполняться.

В этой схеме работы есть плюсы.

Асинхронный HTTP-сервер

php curl асинхронный запрос. Смотреть фото php curl асинхронный запрос. Смотреть картинку php curl асинхронный запрос. Картинка про php curl асинхронный запрос. Фото php curl асинхронный запрос

Это можно сделать. Мы уже научились работать с сокетами в неблокирующем режиме, а 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 еще идет. Подавайте заявки, если хотите поделиться с сообществом своим опытом.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *