Как наивное связывание resume() может переполнить стек?
Этот awaiter связывает корутины: каждая возобновляет следующую. Длинная цепочка из тысяч корутин стабильно переполняет стек.
struct ResumeNext {
std::coroutine_handle<> next;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<>) {
next.resume();
}
void await_resume() {}
};
Найдите ошибку и объясните причину.
Если await_suspend вызывает handle.resume() для следующей корутины, а та возобновляет ещё одну, каждый resume() вкладывается в предыдущий стековый фрейм. Длинная цепочка не разматывается и переполняет стек.
- ✗Вызывать
handle.resume()внутриawait_suspendвместо возврата handle - ✗Думать, что heap-фреймы означают, что цепочки корутин не переполнят стек
- ✗Считать опасность лишь теоретической и игнорировать глубокие цепочки producer/consumer
- →Как возврат
coroutine_handle<>изawait_suspendограничивает глубину стека? - →Почему хвостовой вызов — ключевое отличие безопасного возобновления от опасного?
Опасный паттерн
struct ResumeNext {
std::coroutine_handle<> next;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<>) {
next.resume(); // ❌ вложенный вызов — фрейм стека НЕ снимается
}
void await_resume() {}
};
Когда корутина A делает co_await ResumeNext{B}, B.resume() вызывается поверх стекового фрейма A. Если B сразу делает co_await ResumeNext{C}, добавляется ещё один кадр, и так далее. Цепочка из тысяч корутин — гарантированное переполнение стека.
Исправление: symmetric transfer
struct ResumeNext {
std::coroutine_handle<> next;
bool await_ready() { return false; }
std::coroutine_handle<> await_suspend(std::coroutine_handle<>) {
return next; // ✅ компилятор делает хвостовой переход
}
void await_resume() {}
};
Возврат handle вместо вызова resume() превращает возобновление в хвостовой вызов: текущий кадр снимается перед переходом к next. Глубина стека остаётся постоянной независимо от длины цепочки.