Файберы в TrueAsync
В стандартном PHP файбер (Fiber) — это кооперативный поток с собственным стеком вызовов.
При подключении расширения TrueAsync файбер переключается в корутинный режим:
вместо прямого переключения стеков, файбер получает собственную корутину,
которой управляет планировщик (Scheduler).
Эта статья описывает ключевые изменения в поведении файберов при работе с TrueAsync.
Корутинный режим файбера
При создании new Fiber(callable), если TrueAsync активен, вместо инициализации
контекста переключения стеков создаётся корутина:
fiber->coroutine = ZEND_ASYNC_NEW_COROUTINE(...);
ZEND_COROUTINE_SET_FIBER(fiber->coroutine);
fiber->coroutine->extended_data = fiber;
fiber->coroutine->internal_entry = coroutine_entry_point;
Вызов $fiber->start() не переключает стек напрямую, а ставит корутину в очередь
планировщика через ZEND_ASYNC_ENQUEUE_COROUTINE, после чего вызывающий код
приостанавливается в zend_fiber_await() до завершения или приостановки файбера.
Жизненный цикл refcount корутины
Файбер явно удерживает свою корутину через ZEND_ASYNC_EVENT_ADD_REF:
После конструктора: coroutine refcount = 1 (планировщик)
После start(): coroutine refcount = 2 (планировщик + файбер)
Дополнительный +1 от файбера необходим, чтобы корутина оставалась живой
после завершения, иначе getReturn(), isTerminated() и другие методы
не смогут обратиться к результату.
Освобождение +1 происходит в деструкторе файбера (zend_fiber_object_destroy):
if (ZEND_COROUTINE_IS_FINISHED(coroutine) || !ZEND_COROUTINE_IS_STARTED(coroutine)) {
ZEND_ASYNC_EVENT_RELEASE(&coroutine->event);
}
Параметры Fiber::start() — копирование в heap
Макрос Z_PARAM_VARIADIC_WITH_NAMED при разборе аргументов Fiber::start()
устанавливает fcall->fci.params как указатель прямо в стек VM-фрейма.
В стандартном PHP это безопасно — zend_fiber_execute вызывается немедленно
через переключение стека, и фрейм Fiber::start() ещё жив.
В корутинном режиме fcall->fci.params может стать
висячим указателем, если ожидаемая корутина разрушится первой.
Дать гарантию, что такое никогда не произойдёт, невозможно.
Поэтому после разбора параметров копируем их в heap-память:
if (fiber->coroutine != NULL && fiber->fcall != NULL) {
if (fiber->fcall->fci.param_count > 0) {
uint32_t count = fiber->fcall->fci.param_count;
zval *heap_params = emalloc(sizeof(zval) * count);
for (uint32_t i = 0; i < count; i++) {
ZVAL_COPY(&heap_params[i], &fiber->fcall->fci.params[i]);
}
fiber->fcall->fci.params = heap_params;
}
if (fiber->fcall->fci.named_params) {
GC_ADDREF(fiber->fcall->fci.named_params);
}
}
Теперь coroutine_entry_point
может безопасно использовать и освобождать параметры.
GC для корутинных файберов
Вместо добавления объекта корутины в GC буфер, zend_fiber_object_gc
напрямую обходит стек выполнения корутины и передаёт найденные переменные:
if (fiber->coroutine != NULL) {
zend_execute_data *ex = ZEND_ASYNC_COROUTINE_GET_EXECUTE_DATA(fiber->coroutine);
if (ex != NULL && ZEND_COROUTINE_IS_YIELD(fiber->coroutine)) {
// Обход стека — как для обычного файбера
for (; ex; ex = ex->prev_execute_data) {
// ... добавляем CV в GC буфер ...
}
}
}
Это работает только для состояния YIELD (файбер приостановлен через Fiber::suspend()).
Для других состояний (running, awaiting child) стек активен и обходить его нельзя.
Деструкторы из GC
В стандартном PHP деструкторы объектов, найденных GC, вызываются синхронно
в том же контексте. В TrueAsync GC запускается в отдельной GC-корутине
(см. Сборка мусора в асинхронном контексте).
Это означает:
-
Порядок выполнения — деструкторы выполняются асинхронно, после возврата из
gc_collect_cycles(). -
Fiber::suspend()в деструкторе — невозможен. Деструктор выполняется в GC-корутине, а не в файбере. ВызовFiber::suspend()приведёт к ошибке «Cannot suspend outside of a fiber». -
Fiber::getCurrent()в деструкторе — вернётNULL, так как деструктор выполняется вне контекста файбера.
По этой причине тесты, рассчитанные на синхронное выполнение деструкторов
из GC внутри файбера, помечены как skip для TrueAsync.
Генераторы при shutdown
В стандартном PHP при уничтожении файбера генератор маркируется флагом
ZEND_GENERATOR_FORCED_CLOSE. Это запрещает yield from в finally-блоках —
генератор умирает и не должен создавать новые зависимости.
В TrueAsync корутина получает graceful cancellation, а не принудительное
закрытие. Генератор не маркируется как FORCED_CLOSE, и yield from
в finally-блоках может выполниться. Это известное отличие в поведении.
Пока неясно, стоит ли менять это или нет.