Механизм ожидания и пробуждения корутин
Для того чтобы хранить контекст ожидания корутины, TrueAsync использует структуру Waker. Она служит связующим звеном между корутиной и событиями, на которые она подписана. Благодаря Waker корутина всегда знает, какие именно события она ждёт.
Структура Waker
С целью оптимизации работы с памятью, waker интегрировать непосредственно в структуру корутины (zend_coroutine_t), что позволяет избежать дополнительных аллокаций и упростить управление памятью, хотя в коде используется указатель zend_async_waker_t *waker для обратной совместимости.
Waker хранит список ожидаемых событий, а также агрерирует результат ожидания или исключение.
struct _zend_async_waker_s {
ZEND_ASYNC_WAKER_STATUS status;
// События, которых ждёт корутина
HashTable events;
// События, сработавшие на последней итерации
HashTable *triggered_events;
// Результат пробуждения
zval result;
// Ошибка (если пробуждение вызвано ошибкой)
zend_object *error;
// Точка создания (для отладки)
zend_string *filename;
uint32_t lineno;
// Деструктор
zend_async_waker_dtor dtor;
};Статусы Waker
На каждом этапе жизни корутины Waker находится в одном из пяти состояний:
typedef enum {
ZEND_ASYNC_WAKER_NO_STATUS, // Waker не активен
ZEND_ASYNC_WAKER_WAITING, // Корутина ждёт событий
ZEND_ASYNC_WAKER_QUEUED, // Корутина поставлена в очередь на выполнение
ZEND_ASYNC_WAKER_IGNORED, // Корутина пропущена
ZEND_ASYNC_WAKER_RESULT // Результат доступен
} ZEND_ASYNC_WAKER_STATUS;Корутина начинает с NO_STATUS — Waker существует, но не активен, корутина выполняется. Когда корутина вызывает SUSPEND(), Waker переходит в WAITING, теперь он следит за событиями.
Когда одно из событий срабатывает, Waker переходит в QUEUED: результат сохранён, корутина стоит в очереди Scheduler'а и ждёт переключения контекста.
Статус IGNORED необходим для случая, когда корутина уже стоит в очереди, но должна быть уничтожена. Тогда Scheduler не запускает корутину, а немедленно финализирует состояние.
Когда корутина просыпается, Waker переходит в состояние RESULT. При этом waker->error переходит в EG(exception). В случае отсутствия ошибок, корутина может использовать waker->result. Например, именно result возвращает функция await().
Создание Waker
// Получить waker (создать если нет)
zend_async_waker_t *waker = zend_async_waker_define(coroutine);
// Переинициализировать waker для нового ожидания
zend_async_waker_t *waker = zend_async_waker_new(coroutine);
// С таймаутом и cancellation
zend_async_waker_t *waker = zend_async_waker_new_with_timeout(
coroutine, timeout_ms, cancellation_event);zend_async_waker_new() деструктурирует существующий waker и сбрасывает его в начальное состояние. Это позволяет переиспользовать waker без аллокаций.
Подписка на события
Модуль zend_async_API.c предоставляет несколько готовых функций, чтобы связать корутину с событием:
zend_async_resume_when(
coroutine, // Какую корутину пробуждать
event, // На какое событие подписаться
trans_event, // Передать ownership события
callback, // Callback-функция
event_callback // Корутинный callback (или NULL)
);resume_when — главная функция подписки. Она создаёт zend_coroutine_event_callback_t, привязывает его к событию и к waker'у корутины.
В качестве callback-функции вы можете использовать одну из трёх стандартных, в зависимости от того, как вы хотите пробуждать корутину:
// Успешный результат
zend_async_waker_callback_resolve(event, callback, result, exception);
// Отмена
zend_async_waker_callback_cancel(event, callback, result, exception);
// Таймаут
zend_async_waker_callback_timeout(event, callback, result, exception);