Это не статья о параллельном расширении (или любом другом, которое привносит мощь потоков в PHP) — на самом деле я не думаю (что касается на данный момент) существуют хорошие варианты использования для использования потоков в PHP (есть и другие шаблоны — касающиеся PHP — которые позволяют делать подобные вещи правильно). Но! Есть также доступные инструменты (например, тот, которым я собираюсь поделиться с вами здесь), которые просто трудно игнорировать! И в том-то и дело — вы уже можете делать (по крайней мере одну вещь) асинхронно в PHP — прямо из коробки!
Рассмотрим такой пример:
Вы хотите сделать 4 запроса к внешним (одному или 4 разным) сервисам — какова будет «стоимость времени» всех 4 запросов?
Все знают, но позвольте мне быстро напомнить два возможных сценария:
- мы можем обрабатывать запросы синхронно — тогда общее время равно сумме стоимости всех последующих запросов
- мы можем сделать их асинхронно — общее время будет равно времени самого длинного запроса
См. диаграмму ниже, чтобы визуализировать сценарии, предполагая, что нам нужно сделать 4 запроса, где:
- ответ для первого немедленный (мы можем игнорировать задержку)
- ответ для второго приходит через 1 секунду
- ответ на третий — 2 секунды
- ответ на четвертый — 3 секунды

Какой ответ будет применим к PHP?
Давайте проверим это (объяснение проведенного теста вы можете найти ниже):
➜ docker compose run php Sequence: 0 .. 3 ... end Woke up after 0 second(s) of sleep! Woke up after 1 second(s) of sleep! Woke up after 2 second(s) of sleep! Woke up after 3 second(s) of sleep! real 0m3.099s
3 секунды! Удивлены? Продолжай читать :)
Что именно было протестировано здесь?
В качестве внешнего сервиса я использовал инструмент, который я называю спящим сервером — вы можете отправить запрос этому инструменту (сервису) с количеством секунд в качестве параметра, и он ответит через запрошенное время (подробнее а примеры можно найти в Readme проекта).
Протестированный PHP-код:
$client = new GuzzleHttp\Client();
for($i = $sequenceStart; $i <= $sequenceStop; $i++) {
Utils::task(
fn() => $client->getAsync("{$sleepingUrl}/{$i}")
->then($printResponse)
->wait()
);
}
echo 'end' . PHP_EOL;
И все вместе было связано docker-compose:
services:
sleeping:
build: "https://github.com/lbacik/sleeping-server.git#main"
php:
build: ./php
environment:
SLEEPING_URL: "http://sleeping:3000"
depends_on:
- sleeping
...
Весь код можно найти в репозитории https://github.com/lbacik/php-async.
Давайте сравним результаты PHP (официальное изображение php:8.1) с кодом JS.
JS-версия кода:
for (let i=sequenceStart; i<=sequenceStop; i++) {
fetch(`${sleepingUrl}/${i}`)
.then(response => response.text())
.then(result => console.log(result))
}
console.log('end')
И тест:
➜ docker compose run js Sequence: 0 .. 3 ... end Woke up after 0 second(s) of sleep! Woke up after 1 second(s) of sleep! Woke up after 2 second(s) of sleep! Woke up after 3 second(s) of sleep! real 0m3.182s
Результаты почти такие же :) !
Что нужно объяснить
Я думаю, что есть два (относительно кода PHP):
- Почему эти запросы вообще выполняются? (обратите внимание на строку «конец», напечатанную в выводе скрипта — это последняя строка скрипта!)
- Почему эти запросы выполняются асинхронно?
Первый немного странный — я имею в виду — ответ (по крайней мере, для меня). Позволь мне объяснить:
- мы начинаем с Util::task(), как это было использовано в моем примере — проверьте этот метод (он объявлен в пространстве имен GuzzleHttp\Promise)! Вы можете найти там что-то вроде $queue = self::queue()
- следуйте этой трассировке и проверьте метод queue(). Если очереди не существует, она создается: $queue = new TaskQueue() (что происходит в нашем случае) — открываем TaskQueue конструктор:
public function __construct($withShutdown = true)
{
if ($withShutdown) {
register_shutdown_function(function () {
if ($this->enableShutdown) {
// Only run the tasks if an E_ERROR didn't occur.
$err = error_get_last();
if (!$err || ($err['type'] ^ E_ERROR)) {
$this->run();
}
}
});
}
}
Готово: зарегистрируйте_закрытие_функции!
Сначала это меня удивило (очень!) Но да, если в PHP есть такие возможности, почему бы их не использовать (впрочем, я не уверен в этом 🤔).
И сейчас:
Почему эти запросы выполняются асинхронно?
Потому что библиотека curl это позволяет!
-Z, — parallel
Заставляет curl выполнять свои передачи параллельно, а не обычным последовательным способом.
И PHP тоже использует эту библиотеку — интерпретатор php можно скомпилировать с ней или добавить как расширение — расширения PHP можно проверить php -m:
$ php -m ... curl ...
PHP-расширение Curl предоставляет доступ к системной библиотеке curl — то же самое, что вы можете использовать с интерфейсом cli curl (если я правильно проверил):
$ docker run --rm -ti php:8.1 bash ... # ldd /usr/local/bin/php | grep curl libcurl.so.4 => /usr/lib/x86_64-linux-gnu/libcurl.so.4 ... # ldd /usr/bin/curl | grep libcurl libcurl.so.4 => /usr/lib/x86_64-linux-gnu/libcurl.so.4 ...
Это инструмент, написанный на C (я думаю), который может использовать потоки :) А PHP API предоставляет способ использовать многопоточность, предоставляемую библиотекой libcurl!
А вот и следующая часть — если вы хотите использовать эту функциональность напрямую, я имею в виду то, как ее описывает PHP API, то обсуждаемый код может выглядеть так:
<?php
require_once __DIR__ . '/../vendor/autoload.php';
$sleepingUrl = getenv('SLEEPING_URL') ?: "http://localhost:3000";
$multi = curl_multi_init();
$stillRunning = null;
for ($i = 0; $i <= 3; $i++) {
$curl = curl_init("{$sleepingUrl}/{$i}");
curl_multi_add_handle($multi, $curl);
}
do {
$status = curl_multi_exec($multi, $stillRunning);
if ($stillRunning) {
curl_multi_select($multi);
}
} while ($stillRunning && $status == CURLM_OK);
приведенный выше скрипт можно найти здесь и выполнить/протестировать (извините за этого маленького cli-монстра):
$ docker compose run \ --entrypoint "/bin/bash -c \"time php bin/fetch_mcurl.php\"" php
Да, вы можете использовать это так, но не выглядит ли это намного сложнее, чем решение, представленное выше:
Utils::task(
fn() => $client->getAsync("{$sleepingUrl}/{$i}")
->then($printResponse)
->wait()
);
Как по мне, использование через промис-интерфейс, реализованный разработчиками Guzzle, выглядит намного лучше :)
Послесловия
Так что «асинхронный PHP» возможен! 😉 Интересно, есть ли другие «расширения», обеспечивающие асинхронный функционал? Надо будет как-нибудь проверить (если знаете — дайте знать!) :)