Coroutine Wait and Wake-up Mechanism
To store the waiting context of a coroutine,
TrueAsync uses the Waker structure.
It serves as the link between a coroutine and the events it is subscribed to.
Thanks to the Waker, a coroutine always knows exactly which events it is waiting for.
Waker Structure
For memory optimization purposes, the waker is integrated directly into the coroutine structure (zend_coroutine_t),
which avoids additional allocations and simplifies memory management,
although a zend_async_waker_t *waker pointer is used in the code for backward compatibility.
The Waker holds a list of awaited events and aggregates the wait result or exception.
struct _zend_async_waker_s {
ZEND_ASYNC_WAKER_STATUS status;
// Events the coroutine is waiting for
HashTable events;
// Events that fired on the last iteration
HashTable *triggered_events;
// Wake-up result
zval result;
// Error (if wake-up was caused by an error)
zend_object *error;
// Creation point (for debugging)
zend_string *filename;
uint32_t lineno;
// Destructor
zend_async_waker_dtor dtor;
};
Waker Statuses
At each stage of a coroutine’s life, the Waker is in one of five states:
typedef enum {
ZEND_ASYNC_WAKER_NO_STATUS, // Waker is not active
ZEND_ASYNC_WAKER_WAITING, // Coroutine is waiting for events
ZEND_ASYNC_WAKER_QUEUED, // Coroutine is queued for execution
ZEND_ASYNC_WAKER_IGNORED, // Coroutine was skipped
ZEND_ASYNC_WAKER_RESULT // Result is available
} ZEND_ASYNC_WAKER_STATUS;
A coroutine starts with NO_STATUS – the Waker exists but is not active; the coroutine is executing.
When the coroutine calls SUSPEND(), the Waker transitions to WAITING and begins monitoring events.
When one of the events fires, the Waker transitions to QUEUED: the result is saved,
and the coroutine is placed in the Scheduler queue awaiting a context switch.
The IGNORED status is needed for cases when a coroutine is already in the queue but must be destroyed.
In that case, the Scheduler does not launch the coroutine but immediately finalizes its state.
When the coroutine wakes up, the Waker transitions to the RESULT state.
At this point, waker->error is transferred to EG(exception).
If there are no errors, the coroutine can use waker->result. For example, result is what the
await() function returns.
Creating a Waker
// Get waker (create if it doesn't exist)
zend_async_waker_t *waker = zend_async_waker_define(coroutine);
// Reinitialize waker for a new wait
zend_async_waker_t *waker = zend_async_waker_new(coroutine);
// With timeout and cancellation
zend_async_waker_t *waker = zend_async_waker_new_with_timeout(
coroutine, timeout_ms, cancellation_event);
zend_async_waker_new() destructs the existing waker
and resets it to its initial state. This allows reusing
the waker without allocations.
Subscribing to Events
The zend_async_API.c module provides several ready-made functions to bind a coroutine to an event:
zend_async_resume_when(
coroutine, // Which coroutine to wake
event, // Which event to subscribe to
trans_event, // Transfer event ownership
callback, // Callback function
event_callback // Coroutine callback (or NULL)
);
resume_when is the main subscription function.
It creates a zend_coroutine_event_callback_t, binds it
to the event and to the coroutine’s waker.
As the callback function, you can use one of three standard ones, depending on how you want to wake the coroutine:
// Successful result
zend_async_waker_callback_resolve(event, callback, result, exception);
// Cancellation
zend_async_waker_callback_cancel(event, callback, result, exception);
// Timeout
zend_async_waker_callback_timeout(event, callback, result, exception);