Асинхронные промисы
OxPHP запускает PHP-замыкания в фоновых потоках — в выделенном пуле, отдельном от пула HTTP-воркеров. Долго выполняющаяся работа уходит туда, вместо того чтобы блокировать обработку запросов.
Как это работает
- Отправка — вызовите
oxphp_async()с замыканием и необязательными аргументами. OxPHP сериализуетuse-переменные замыкания и его аргументы, отправляет их в асинхронный пул и сразу возвращает идентификатор промиса - Выполнение — выделенный поток асинхронного воркера десериализует данные, выполняет замыкание и сериализует результат
- Ожидание — вызовите
oxphp_async_await()с идентификатором промиса. В режиме воркеров с файберами текущий файбер приостанавливается, а другие запросы продолжают выполняться в том же потоке. В традиционном режиме поток воркера блокируется, пока результат не будет готов - Очистка — все промисы, которые не были явно ожиданы, автоматически отменяются и очищаются в конце запроса
Конфигурация
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
ASYNC_WORKERS |
0 (отключено) |
Количество выделенных потоков асинхронных воркеров. Установите 0, чтобы полностью отключить асинхронный пул |
ASYNC_QUEUE_CAPACITY |
0 (авто) |
Максимальное число ожидающих асинхронных задач. При 0 по умолчанию равно ASYNC_WORKERS × 64 |
ASYNC_MAX_FIBERS |
256 |
Ограничение на число одновременных файберов асинхронных задач в расчёте на одного воркера. Глобальный для процесса лимит задач в работе (в очереди + запущенных) равен ASYNC_MAX_FIBERS × ASYNC_WORKERS; отправка сверх него немедленно отклоняется (без блокировки) с OxPHP\Async\AsyncException, поэтому композиция с ветвлением (fan-out) не может зайти во взаимоблокировку, ожидая ёмкость, которую сама удерживает |
Асинхронный пул по умолчанию отключён (ASYNC_WORKERS=0). Когда пул отключён, все четыре асинхронные функции существуют, но при вызове выбрасывают OxPHP\Async\AsyncException. Установите ASYNC_WORKERS в значение больше 0, чтобы включить фоновое выполнение.
Отправка задач
Передайте замыкание и необязательные аргументы в oxphp_async(). Функция сразу возвращает идентификатор промиса (целое число):
<?php
$promise = oxphp_async(function (string $url) {
return file_get_contents($url);
}, 'https://api.example.com/data');
// The closure is running in the background.
// Do other work here...
$result = oxphp_async_await($promise);
echo $result;Передача данных в замыкания
Для передачи данных используйте use-переменные или аргументы функции. Поддерживаются только скалярные типы и массивы:
<?php
$apiKey = 'sk-abc123';
$ids = [1, 2, 3];
$promise = oxphp_async(function () use ($apiKey, $ids) {
// $apiKey and $ids are available here
return count($ids);
});Ожидание результатов
Один промис
<?php
$result = oxphp_async_await($promise); // Wait indefinitely
$result = oxphp_async_await($promise, 5.0); // Wait up to 5 secondsТаймаут 0.0 (значение по умолчанию) означает бесконечное ожидание. По истечении таймаута выбрасывается OxPHP\Async\TimeoutException.
Все промисы
oxphp_async_await_all() ожидает каждый промис и возвращает ассоциативный массив с ключами по идентификаторам промисов:
<?php
$p1 = oxphp_async(fn() => file_get_contents('https://api.example.com/users'));
$p2 = oxphp_async(fn() => file_get_contents('https://api.example.com/orders'));
$results = oxphp_async_await_all([$p1, $p2], 10.0);
$users = $results[$p1];
$orders = $results[$p2];oxphp_async_await_all() ожидает промисы последовательно, в порядке их следования в массиве. Все замыкания выполняются конкурентно в асинхронном пуле, но вызывающий поток собирает результаты по одному.
Первый завершившийся промис (race)
oxphp_async_await_race() возвращает управление, как только завершится любой из промисов — независимо от того, выполнился он или был отклонён:
<?php
$p1 = oxphp_async(fn() => fetch_from_primary_db());
$p2 = oxphp_async(fn() => fetch_from_replica_db());
$winner = oxphp_async_await_race([$p1, $p2], 5.0);
// $winner = ['id' => int, 'value' => mixed]
echo "Promise {$winner['id']} won: {$winner['value']}";Промисы, не ставшие победителями, после возврата из oxphp_async_await_race() по-прежнему можно ожидать по отдельности. Если победивший промис был отклонён, выбрасывается OxPHP\Async\AsyncException — проигравшие промисы при этом остаются доступными для ожидания. Это аналог JavaScript-функции Promise.race.
Первый выполнившийся промис
oxphp_async_await_any() возвращает управление, как только один из промисов ВЫПОЛНИТСЯ. Отклонения накапливаются и становятся видимыми, только если отклонены все промисы. Это аналог JavaScript-функции Promise.any — полезно для сценариев с запасными вариантами / резервированием («любое зеркало, которое ответит»).
<?php
$mirror_a = oxphp_async(fn() => fetch('https://mirror-a.example.com/data'));
$mirror_b = oxphp_async(fn() => fetch('https://mirror-b.example.com/data'));
$mirror_c = oxphp_async(fn() => fetch('https://mirror-c.example.com/data'));
try {
$winner = oxphp_async_await_any([$mirror_a, $mirror_b, $mirror_c], 5.0);
// ['id' => one of the input ids, 'value' => its result]
} catch (\OxPHP\Async\AggregateAsyncException $e) {
foreach ($e->getErrors() as $i => $err) {
// $err keyed by input position 0..N-1
}
foreach ($e->getErrorMap() as $promise_id => $err) {
// $err keyed by promise id
}
} catch (\OxPHP\Async\TimeoutException $e) {
foreach ($e->getPartialErrors() as $promise_id => $err) {
// promises that already rejected before the deadline
}
$cancelled = $e->getCancelledPromiseIds();
// Promise ids that had not settled at the deadline. The cancel flag
// is set on each AND their receivers were dropped — passing any of
// these ids to oxphp_async_await*() afterwards throws "unknown or
// already-awaited promise id". Treat the list as an audit trail, not
// a resumable queue.
}Промисы, не ставшие победителями и всё ещё ожидавшие выполнения в момент победы, остаются доступными для ожидания по отдельности. А промисы, которые были отклонены до появления победителя, — нет: их результаты были поглощены при накоплении в качестве потенциальных ошибок.
Типы исключений
| Класс | Выбрасывается | Примечания |
|---|---|---|
OxPHP\Async\AsyncException |
oxphp_async_await(), oxphp_async_await_all(), oxphp_async_await_race() |
Одиночная ошибка с сообщением и необязательными сведениями об исходном исключении. |
OxPHP\Async\TimeoutException |
Все четыре await-функции при истечении срока ожидания | Расширяет AsyncException. При таймаутах oxphp_async_await_any() заполняются getPartialErrors() и getCancelledPromiseIds(); в остальных случаях обе возвращают []. |
OxPHP\Async\AggregateAsyncException |
oxphp_async_await_any(), когда отклонены все промисы |
Расширяет AsyncException. Предоставляет getErrors() (позиционный, с ключами 0..N-1), getErrorMap() (с ключами по id), getPromiseIds(). |
Обработка ошибок
Исключения, выброшенные внутри асинхронного замыкания, перехватываются и повторно выбрасываются в момент ожидания как OxPHP\Async\AsyncException:
<?php
$promise = oxphp_async(function () {
throw new \RuntimeException('Something failed');
});
try {
$result = oxphp_async_await($promise);
} catch (\OxPHP\Async\AsyncException $e) {
// "Async task failed: [RuntimeException] Something failed"
echo $e->getMessage();
}Вызовы exit() и die() внутри асинхронного замыкания также перехватываются и преобразуются в OxPHP\Async\AsyncException. Асинхронный воркер при этом продолжает работу и обрабатывает новые задачи.
Иерархия исключений
\Exception
└── OxPHP\Async\AsyncException # All async errors
├── OxPHP\Async\TimeoutException # Timeout-specific
└── OxPHP\Async\AggregateAsyncException # Multiple failures (await_all / await_any)Интеграция с файберами
В режиме воркеров oxphp_async_await() взаимодействует с планировщиком файберов OxPHP. Вместо того чтобы блокировать поток воркера, текущий файбер приостанавливается на время ожидания результата. Планировщик возобновляет его, как только результат готов, поэтому другие запросы продолжают выполняться в том же потоке.
В традиционном режиме (без файла воркера) oxphp_async_await() блокирует поток воркера синхронно. Пока воркер ждёт, он не может обрабатывать другие запросы.
Для максимальной производительности сочетайте асинхронные промисы с режимом воркеров:
<?php
// worker.php
require __DIR__ . '/../vendor/autoload.php';
oxphp_worker(function () {
// These two API calls run concurrently on the async pool
// while the fiber suspends — the worker thread is free for other requests
$p1 = oxphp_async(fn() => file_get_contents('https://api.example.com/users'));
$p2 = oxphp_async(fn() => file_get_contents('https://api.example.com/orders'));
$results = oxphp_async_await_all([$p1, $p2]);
echo json_encode($results);
});Композиция (вложенные асинхронные вызовы)
Асинхронная задача может сама вызвать oxphp_async() и дождаться результата. Поскольку каждая задача выполняется внутри файбера планировщика, ожидание вложенного промиса приостанавливает файбер самой задачи и освобождает её воркер для выполнения вложенной задачи — так задача может разветвляться на дочерние, не удерживая воркер во время ожидания.
<?php
$p = oxphp_async(function (): int {
// Dispatched and awaited from inside an async task
$inner = oxphp_async(fn () => 21);
return oxphp_async_await($inner) * 2;
});
$result = oxphp_async_await($p); // 42oxphp_async_await_all(), oxphp_async_await_race() и oxphp_async_await_any() точно так же можно вызывать изнутри файбера задачи. Число одновременных файберов задач (в очереди + запущенных) ограничено значением ASYNC_MAX_FIBERS × ASYNC_WORKERS; отправка, которая превысила бы этот лимит, немедленно отклоняется с OxPHP\Async\AsyncException, а не блокируется, поэтому ветвление не может привести к взаимоблокировке из-за ожидания ёмкости, которую само же удерживает.
Ограничения
Асинхронные замыкания выполняются в отдельных потоках. Это накладывает ограничения на то, какие данные могут пересекать границу потока:
| Разрешено | Не разрешено |
|---|---|
null, bool, int, float, string |
Обычные объекты (любой класс, не реализующий OxPHP\Shared\Shareable) |
| Массивы скалярных типов | Ресурсы (файловые дескрипторы, подключения к БД, потоки) |
| Вложенные массивы скаляров | Замыкания, чей use захватывает объекты, не являющиеся Shareable |
Экземпляры Shared\* (Counter, Map, Channel, Atomic, Flag, Mutex, Once, Pool, Registry) и другие классы, реализующие OxPHP\Shared\Shareable |
Дополнительные ограничения:
- Только пользовательские функции — замыкание должно быть определено пользователем, а не быть обёрткой вокруг встроенной функции
- Накладные расходы на сериализацию — аргументы и возвращаемые значения сериализуются при пересечении границы потока. Большие массивы или строки увеличивают задержку
- Нет разделяемого состояния для обычных PHP-значений — у каждого асинхронного воркера собственное PHP-окружение. Обычные переменные, массивы и экземпляры классов копируются (или отклоняются) при пересечении границы. Используйте примитивы разделяемого состояния (
Shared\Counter,Shared\Map,Shared\Channel, …) для передачи ссылок, видимых обоим потокам
Пример для Docker
services:
app:
image: ghcr.io/oxphp/oxphp:0.10.0
ports:
- "80:80"
environment:
- DOCUMENT_ROOT=/var/www/html/public
- WORKER_MODE_ENABLED=true
- ENTRY_FILE=worker.php
- ASYNC_WORKERS=4
- ASYNC_QUEUE_CAPACITY=256Устранение неполадок
"Async pool is disabled. Set ASYNC_WORKERS > 0 to enable."
Асинхронный пул не настроен. Когда ASYNC_WORKERS=0 (значение по умолчанию), асинхронные функции зарегистрированы, но при каждом вызове выбрасывают OxPHP\Async\AsyncException.
Решение: Установите ASYNC_WORKERS в положительное значение:
ASYNC_WORKERS=4"Failed to dispatch async task (pool full)"
Асинхронный пул работает, но все слоты очереди заняты либо достигнут глобальный для процесса лимит задач в работе (ASYNC_MAX_FIBERS × ASYNC_WORKERS, охватывающий задачи в очереди + запущенные). Оба случая при отправке выбрасывают OxPHP\Async\AsyncException и увеличивают счётчик oxphp_async_tasks_rejected_total.
Проверка: Убедитесь, что пул принимает задачи:
curl -s http://localhost:9090/config | jq '.async_workers'Решение: Увеличьте ASYNC_WORKERS или ASYNC_QUEUE_CAPACITY.
"Cannot pass object values in use-vars to async closure"
Объекты нельзя сериализовать через границу потоков.
Решение: Извлеките нужные скалярные данные перед отправкой:
<?php
// Wrong: passing an object
$promise = oxphp_async(function () use ($user) { ... });
// Correct: passing scalar data extracted from the object
$userId = $user->getId();
$userName = $user->getName();
$promise = oxphp_async(function () use ($userId, $userName) { ... });Ожидание зависает в традиционном режиме
В традиционном режиме oxphp_async_await() блокирует поток воркера. Если все PHP-воркеры заблокированы в ожидании асинхронных результатов, сервер перестаёт обрабатывать запросы.
Решение: Включите режим воркеров (WORKER_MODE_ENABLED=true), чтобы oxphp_async_await() приостанавливал файбер вместо блокировки потока.
Таймауты отменяют брошенную задачу
OxPHP\Async\TimeoutException выбрасывается на стороне ожидания в тот момент, когда истекает срок. Фоновая задача больше не остаётся выполняться без наблюдения: задача, припаркованная в oxphp_sleep() или приостановленная в ожидании дочернего промиса, возобновляется и сворачивается (её блоки finally выполняются), а задача, нагружающая процессор и никогда не уступающая управление, прерывается на границе опкода — так что отмена выполняется по принципу best-effort с небольшой ограниченной задержкой, а не мгновенно. Та же отмена применяется к промисам, которые бросает oxphp_async_await_all(), и к проигравшим в oxphp_async_await_race() / oxphp_async_await_any(). Задача, которую всё же не удаётся прервать вовремя, считается застрявшей (stranded) — отменённой, но вычищаемой в конце запроса, что может продлить RSHUTDOWN на несколько секунд; следите за oxphp_async_tasks_stranded_total.
Рекомендации
- Всегда задавайте таймауты для вызовов
oxphp_async_await()в продакшене, чтобы избежать бесконечного ожидания - Используйте режим воркеров, чтобы получить неблокирующее ожидание на файберах вместо блокировки потока воркера
- Держите замыкания небольшими — отправляйте узкие единицы работы, а не целые обработчики запросов
- Извлекайте скаляры перед отправкой — доставайте идентификаторы, строки и значения конфигурации из объектов, прежде чем передавать их в замыкание
- Следите за асинхронным пулом — отслеживайте
oxphp_async_tasks_rejected_totalв метриках Prometheus. Если число отказов растёт, увеличьтеASYNC_WORKERSилиASYNC_QUEUE_CAPACITY
См. также
- Режим воркеров — постоянные PHP-процессы с конкурентностью на файберах
- PHP-функции — справочник по
oxphp_async(),oxphp_async_await()и связанным функциям - Метрики — метрики Prometheus для асинхронного пула
- Справочник по конфигурации —
ASYNC_WORKERSиASYNC_QUEUE_CAPACITY