Асинхронні проміси
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, тож композиція з розгалуженням не може зайти в дедлок, очікуючи на ресурс, який сама ж утримує |
Асинхронний пул типово вимкнено (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 — на решту промісів однаково можна очікувати. Це аналог Promise.race з JavaScript.
Перший успішно виконаний проміс
oxphp_async_await_any() повертає керування, щойно будь-який проміс ВИКОНАЄТЬСЯ успішно. Відхилення накопичуються й стають видимими, лише якщо відхилено всі проміси. Це аналог Promise.any з JavaScript — корисний для сценаріїв із резервуванням («будь-яке дзеркало, що відповість»).
<?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() (ключі — ідентифікатори), 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 # Every promise rejected (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 зарезервовано за типами Shared\*: клас, написаний на PHP, не може ні реалізувати його, ні успадковувати клас, який його реалізує |
| Масиви скалярних типів | Ресурси (дескриптори файлів, зʼєднання з БД, потоки) |
| Вкладені скалярні масиви | Замикання, чий use захоплює обʼєкти, що не є Shareable |
Екземпляри Shared\* (Counter, Map, Channel, Atomic, Flag, Mutex, Once, Pool, Registry) |
Додаткові обмеження:
- Лише користувацькі функції — замикання має бути визначене користувачем, а не обгорткою над вбудованою функцією
- Накладні витрати на серіалізацію — аргументи й значення, що повертаються, серіалізуються під час перетину межі потоку. Великі масиви чи рядки збільшують затримку
- Немає спільного стану для звичайних значень PHP — кожен асинхронний воркер має власне середовище PHP. Звичайні змінні, масиви й екземпляри класів копіюються (або відхиляються) на межі. Щоб передавати посилання, видимі обом потокам, використовуйте примітиви спільного стану (
Shared\Counter,Shared\Map,Shared\Channel, …) - Усередині задачі немає завершення скрипту — задача не є запитом, тому
register_shutdown_function(), викликана з асинхронного замикання, реєструє зворотний виклик, який ніколи не виконається: немає відповіді, у яку він міг би писати, і немає запиту, завершенням якого він міг би бути, а після завершення задачі його буде відкинуто. Виконуйте цю роботу наприкінці самого замикання - Усередині задачі немає дедлайну виконання —
max_execution_timeне обмежує те, що виконує задача, так само як іset_time_limit(), викликана всередині неї. Обидва виклики й далі завершуються успішно (set_time_limit()повертаєtrue, аini_set()— попереднє значення, як і будь-де), і значення так само читається черезini_get('max_execution_time'), але ніщо його не відлічує. Рушій тримає один таймер виконання на потік, а потік пулу виконує всі задачі свого життя всередині одного-єдиного запиту, який він відкрив під час запуску. Дедлайн, заведений там, не належав би жодній конкретній задачі й завершив би ту, що саме виконувала PHP-код, коли він сплив, — зокрема й задачу, яка взагалі не просила жодного ліміту. Натомість обмежуйте задачу з боку того, хто на неї чекає: аргументом тайм-аутуoxphp_async_await()та її варіантів, який скасовує задачу, на яку перестали чекати (див. «Тайм-аути скасовують покинуту задачу» в розділі Усунення несправностей). Одне директиву все ж читає: з увімкненимRUNTIME_HOOKS=streamsочікування на підключення, яке використовує інший файбер, обмежене меншим ізmax_execution_timeіdefault_socket_timeout. Задача, що знижує це значення, скорочує таке очікування і для себе, і для задач, які виконуватимуться в цьому потоці після неї, адже ini-значення, задане задачею, не відкочується до завершення запиту, а запит потоку пулу не завершується
Приклад для Docker
services:
app:
image: ghcr.io/oxphp/oxphp:0.13.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(). Задача, яку все одно не вдається вчасно перервати, стає застряглою — скасованою, але дочікуваною наприкінці запиту, що може подовжити RSHUTDOWN на кілька секунд; стежте за oxphp_async_tasks_stranded_total.
Рекомендації
- Завжди встановлюйте тайм-аути для викликів
oxphp_async_await()у продакшені, щоб уникнути нескінченного очікування - Використовуйте режим воркера, щоб отримати неблокувальне очікування на основі файберів замість блокування потоку воркера
- Робіть замикання невеликими — відправляйте сфокусовані одиниці роботи, а не цілі обробники запитів
- Витягуйте скалярні значення перед відправленням — дістаньте ідентифікатори, рядки й значення конфігурації з обʼєктів, перш ніж передавати їх у замикання
- Моніторте асинхронний пул — перевіряйте
oxphp_async_tasks_rejected_totalу метриках Prometheus. Якщо кількість відхилень зростає, збільштеASYNC_WORKERSабоASYNC_QUEUE_CAPACITY
Промпти для вашого агента
Ці промпти передбачають, що до агента підключено MCP-сервер документації OxPHP, — потрібні сторінки він прочитає сам.
Перенеси повільну роботу з моїх обробників запитів в асинхронний пул OxPHP. Прочитай сторінку `features/async-promises` через MCP-сервер документації OxPHP.
Вибери виклики в цьому репозиторії, які варто відправляти в пул, перепиши їх як замикання, що не передають через межу потоку нічого, окрім даних, і назви розмір пулу та місткість черги, з якими їх слід запускати.Див. також
- Режим воркера — постійні PHP-воркери з конкурентністю на основі файберів
- PHP-функції — довідник щодо
oxphp_async(),oxphp_async_await()і повʼязаних функцій - Метрики — метрики Prometheus для асинхронного пулу
- Довідник із налаштувань —
ASYNC_WORKERSіASYNC_QUEUE_CAPACITY