Класс Async\Coroutine
(PHP 8.6+, True Async 1.0)
Корутины в TrueAsync
Когда обычная функция вызывает операцию ввода-вывода fread, fwrite (чтение файла или сетевой запрос),
управление передаётся ядру операционной системы, и PHP блокируется, пока операция не завершится.
Но если функция выполняется в корутине и вызывает операцию ввода-вывода,
блокируется только корутина, а не весь процесс PHP.
При этом управление передаётся другой корутине, если такая имеется.
В этом смысле корутины очень похожи на потоки операционной системы (threads), но управляются в пользовательском пространстве, а не ядром ОС.
Ещё одно важное отличие состоит в том, что корутины разделяют процессорное время по очереди, самостоятельно уступая управление, в то время как потоки могут быть прерваны в любой момент.
Корутины TrueAsync выполняются в рамках одного потока,
не являются параллельными. Из этого следует несколько важных последствий:
- Переменные можно свободно читать и изменять из разных корутин без блокировок, так как они не выполняются одновременно.
- Корутины не могут одновременно использовать несколько ядер процессора.
- Если одна корутина выполняет долгую синхронную операцию, она блокирует весь процесс, так как не уступает управление другим корутинам.
Создание корутины
Корутина создаётся с помощью функции spawn():
use function Async\spawn;
// Создаем корутину
$coroutine = spawn(function() {
echo "Привет из корутины!\n";
return 42;
});
// $coroutine - это объект типа Async\Coroutine
// Корутина уже запланирована к выполнению
После того как spawn вызван, функция будет выполнена асинхронно планировщиком так скоро как только возможно.
Передача параметров
Функция spawn принимает callable и любые параметры, которые будут переданы в эту функцию,
передаются callable в момент старта функции.
function fetchUser(int $userId) {
return file_get_contents("https://api/users/$userId");
}
// Передаем функцию и параметры
$coroutine = spawn(fetchUser(...), 123);
Получение результата
Чтобы получить результат корутины, используйте await():
$coroutine = spawn(function() {
sleep(2);
return "Готово!";
});
echo "Корутина запущена\n";
// Ждем результата
$result = await($coroutine);
echo "Результат: $result\n";
Важно: await() блокирует выполнение текущей корутины, но не весь PHP процесс.
Другие корутины продолжают работать.
Жизненный цикл корутины
Корутина проходит несколько состояний:
- Queued — создана через
spawn(), ожидает запуска планировщиком - Running — выполняется в данный момент
- Suspended — приостановлена, ожидает I/O или
suspend() - Completed — завершила выполнение (с результатом или исключением)
- Cancelled — отменена через
cancel()
Проверка состояния
$coro = spawn(longTask(...));
var_dump($coro->isQueued()); // true - ожидает запуска
var_dump($coro->isStarted()); // false - ещё не начала
suspend(); // даём корутине запуститься
var_dump($coro->isStarted()); // true - корутина начала работу
var_dump($coro->isRunning()); // false - сейчас не выполняется
var_dump($coro->isSuspended()); // true - приостановлена, ждёт чего-то
var_dump($coro->isCompleted()); // false - ещё не закончила
var_dump($coro->isCancelled()); // false - не отменена
Приостановка: suspend
Ключевое слово suspend останавливает корутину и передает управление планировщику:
spawn(function() {
echo "До suspend\n";
suspend(); // Останавливаемся здесь
echo "После suspend\n";
});
echo "Основной код\n";
// Вывод:
// До suspend
// Основной код
// После suspend
Корутина остановилась на suspend, управление вернулось в основной код. Позже планировщик возобновил корутину.
suspend с ожиданием
Обычно suspend используется для ожидания какого-то события:
spawn(function() {
echo "Делаю HTTP-запрос\n";
$data = file_get_contents('https://api.example.com/data');
// Внутри file_get_contents неявно вызывается suspend
// Пока идет сетевой запрос, корутина приостановлена
echo "Получил данные: $data\n";
});
PHP автоматически приостанавливает корутину на I/O операциях. Вам не нужно вручную писать suspend.
Отмена корутины
$coro = spawn(function() {
try {
echo "Начинаю долгую работу\n";
for ($i = 0; $i < 100; $i++) {
Async\sleep(100); // Спим 100ms
echo "Итерация $i\n";
}
echo "Закончил\n";
} catch (Async\AsyncCancellation $e) {
echo "Меня отменили на итерации\n";
}
});
// Даем корутине поработать 1 секунду
Async\sleep(1000);
// Отменяем
$coro->cancel();
// Корутина получит AsyncCancellation при следующем await/suspend
Важно: Отмена работает кооперативно. Корутина должна проверять отмену (через await, sleep, или suspend). Нельзя убить корутину силой.
Множественные корутины
Запускайте сколько угодно:
$tasks = [];
for ($i = 0; $i < 10; $i++) {
$tasks[] = spawn(function() use ($i) {
$result = file_get_contents("https://api/data/$i");
return $result;
});
}
// Ждем все корутины
$results = array_map(fn($t) => await($t), $tasks);
echo "Загрузили " . count($results) . " результатов\n";
Все 10 запросов идут конкурентно. Вместо 10 секунд (по секунде каждый) выполнится за ~1 секунду.
Обработка ошибок
Ошибки в корутинах обрабатываются обычным try-catch:
$coro = spawn(function() {
throw new Exception("Упс!");
});
try {
$result = await($coro);
} catch (Exception $e) {
echo "Поймали ошибку: " . $e->getMessage() . "\n";
}
Если не поймать ошибку, она всплывет в родительский scope:
$scope = new Async\Scope();
$scope->spawn(function() {
throw new Exception("Ошибка в корутине!");
});
try {
$scope->awaitCompletion();
} catch (Exception $e) {
echo "Ошибка всплыла в scope: " . $e->getMessage() . "\n";
}
Корутина = объект
Корутина — это полноценный PHP объект. Можно передавать куда угодно:
function startBackgroundTask(): Async\Coroutine {
return spawn(function() {
// Долгая работа
Async\sleep(10000);
return "Результат";
});
}
$task = startBackgroundTask();
// Передаем в другую функцию
processTask($task);
// Или сохраняем в массив
$tasks[] = $task;
// Или в свойство объекта
$this->backgroundTask = $task;
Вложенные корутины
Корутины могут запускать другие корутины:
spawn(function() {
echo "Родительская корутина\n";
$child1 = spawn(function() {
echo "Дочерняя корутина 1\n";
return "Результат 1";
});
$child2 = spawn(function() {
echo "Дочерняя корутина 2\n";
return "Результат 2";
});
// Ждем обе дочерние корутины
$result1 = await($child1);
$result2 = await($child2);
echo "Родитель получил: $result1 и $result2\n";
});
Finally: гарантированная очистка
Даже если корутину отменят, finally выполнится:
spawn(function() {
$file = fopen('data.txt', 'r');
try {
while ($line = fgets($file)) {
processLine($line);
suspend(); // Может быть отменено здесь
}
} finally {
// Гарантированно закроем файл
fclose($file);
echo "Файл закрыт\n";
}
});
Отладка корутин
Получить стек вызовов
$coro = spawn(function() {
doSomething();
});
// Получаем стек вызовов корутины
$trace = $coro->getTrace();
print_r($trace);
Узнать, где корутина создана
$coro = spawn(someFunction(...));
// Где был вызван spawn()
echo "Корутина создана в: " . $coro->getSpawnLocation() . "\n";
// Вывод: "Корутина создана в: /app/server.php:42"
// Или как массив [filename, lineno]
[$file, $line] = $coro->getSpawnFileAndLine();
Узнать, где корутина приостановлена
$coro = spawn(function() {
file_get_contents('https://api.example.com/data'); // suspend здесь
});
suspend(); // даём корутине запуститься
echo "Приостановлена в: " . $coro->getSuspendLocation() . "\n";
// Вывод: "Приостановлена в: /app/server.php:45"
[$file, $line] = $coro->getSuspendFileAndLine();
Информация об ожидании
$coro = spawn(function() {
Async\delay(5000);
});
suspend();
// Узнать, что корутина ожидает
$info = $coro->getAwaitingInfo();
print_r($info);
Очень полезно для отладки — сразу видно, откуда взялась корутина и где она остановилась.
Корутины vs Потоки
| Корутины | Потоки (threads) |
|---|---|
| Легковесные | Тяжелые |
| Быстрое создание (<1μs) | Медленное создание (~1ms) |
| Один поток ОС | Много потоков ОС |
| Кооперативная многозадачность | Вытесняющая многозадачность |
| Нет race conditions | Есть race conditions |
| Нужны await точки | Могут прерваться где угодно |
| Для I/O операций | Для CPU-вычислений |
Отложенная отмена с protect()
Если корутина находится внутри защищённой секции protect(), отмена откладывается до завершения защищённого блока:
$coro = spawn(function() {
$result = protect(function() {
// Критическая операция — отмена отложена
$db->beginTransaction();
$db->execute('INSERT INTO logs ...');
$db->commit();
return "saved";
});
// Отмена произойдёт здесь, после выхода из protect()
echo "Результат: $result\n";
});
suspend();
$coro->cancel(); // Отмена отложена — protect() завершится полностью
Флаг isCancellationRequested() становится true сразу, а isCancelled() — только после фактического завершения корутины.
Обзор класса
final class Async\Coroutine implements Async\Completable {
/* Идентификация */
public getId(): int
/* Приоритет */
public asHiPriority(): Coroutine
/* Контекст */
public getContext(): Async\Context
/* Результат и ошибки */
public getResult(): mixed
public getException(): mixed
/* Состояние */
public isStarted(): bool
public isQueued(): bool
public isRunning(): bool
public isSuspended(): bool
public isCompleted(): bool
public isCancelled(): bool
public isCancellationRequested(): bool
/* Управление */
public cancel(?Async\AsyncCancellation $cancellation = null): void
public finally(\Closure $callback): void
/* Отладка */
public getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0): ?array
public getSpawnFileAndLine(): array
public getSpawnLocation(): string
public getSuspendFileAndLine(): array
public getSuspendLocation(): string
public getAwaitingInfo(): array
}
Содержание
- Coroutine::getId — Получить уникальный идентификатор корутины
- Coroutine::asHiPriority — Пометить корутину как высокоприоритетную
- Coroutine::getContext — Получить локальный контекст корутины
- Coroutine::getResult — Получить результат выполнения
- Coroutine::getException — Получить исключение корутины
- Coroutine::isStarted — Проверить, запущена ли корутина
- Coroutine::isQueued — Проверить, ожидает ли корутина в очереди
- Coroutine::isRunning — Проверить, выполняется ли корутина прямо сейчас
- Coroutine::isSuspended — Проверить, приостановлена ли корутина
- Coroutine::isCompleted — Проверить, завершена ли корутина
- Coroutine::isCancelled — Проверить, была ли корутина отменена
- Coroutine::isCancellationRequested — Проверить, запрошена ли отмена
- Coroutine::cancel — Отменить корутину
- Coroutine::finally — Зарегистрировать обработчик завершения
- Coroutine::getTrace — Получить стек вызовов приостановленной корутины
- Coroutine::getSpawnFileAndLine — Получить файл и строку создания
- Coroutine::getSpawnLocation — Получить место создания как строку
- Coroutine::getSuspendFileAndLine — Получить файл и строку приостановки
- Coroutine::getSuspendLocation — Получить место приостановки как строку
- Coroutine::getAwaitingInfo — Получить информацию об ожидании