Назовите механизм, определяющий, начнёт ли тело корутины выполняться сразу после вызова или останется приостановленным.
Эту семантику задаёт метод promise_type::initial_suspend(). Если он возвращает std::suspend_never, тело корутины начинает выполняться сразу; если std::suspend_always, вызов создаёт корутину в приостановленном состоянии, а выполнение начнётся только после resume() через сохранённый дескриптор.
Корутины появились в стандарте C++20, чтобы поддержать кооперативное приостанавливание и возобновление функций без ручного управления состоянием. До этого подобное поведение обычно реализовывали через конечные автоматы, callback-функции или специализированные библиотеки.
Разделение на promise_type, кадр корутины и awaiter позволяет библиотеке определить модель выполнения: eager — начать работу при вызове, или lazy — отложить её до явного запуска.
Вызов корутины не обязательно означает немедленное выполнение её тела. Он создаёт или подготавливает кадр корутины, объект promise_type и возвращаемый объект, после чего решение о первом запуске принимает initial_suspend().
Ошибка в понимании этого механизма приводит к преждевременному выполнению операций, неожиданному отсутствию побочных эффектов или корутинам, которые вообще никогда не запускаются. Для ленивой корутины возвращаемый объект обязан надёжно владеть дескриптором и предоставить способ возобновления.
После вызова корутины компилятор создаёт её кадр и объект promise_type, получает возвращаемый объект через get_return_object(), а затем применяет awaiter, возвращённый initial_suspend().
std::suspend_never означает, что начальная точка приостановки сразу считается пройденной: выполнение тела начинается до возврата из вызова корутины. std::suspend_always оставляет корутину приостановленной; вызывающий код получает объект, обычно содержащий std::coroutine_handle, и должен вызвать resume().
Пример ленивой корутины:
При таком initial_suspend() сначала выполняется вывод after call, а затем после resume() — тело корутины. Если заменить его на std::suspend_never, тело начнёт выполняться внутри work() до возврата управления в main.
initial_suspend() не определяет последующие точки остановки: их задают co_await, co_yield и завершение корутины. Также он не управляет временем жизни кадра. При final_suspend() == std::suspend_always корутина остаётся завершённой, но её кадр можно безопасно уничтожить через destroy(); при этом дескриптор должен принадлежать объекту, который корректно управляет его временем жизни.
Выбор между режимами — это компромисс. Ленивый запуск удобен для задач, которые должны начинаться только после планирования или явного запроса данных, но требует протокола запуска и владения дескриптором. Немедленный запуск проще для fire-and-forget-сценариев, однако усложняет контроль над моментом начала работы и требует особенно внимательно проектировать время жизни захватываемых объектов.
Команда реализует асинхронный запрос, возвращающий объект задачи. Запрос не должен отправляться при простом создании объекта: сначала его нужно передать планировщику, который выберет поток и момент запуска.
Рассматривались два варианта. При std::suspend_never запрос стартовал бы немедленно, что упрощает интерфейс, но нарушает контракт планировщика и может выполнять работу в неожиданном потоке. При std::suspend_always объект задачи можно поставить в очередь, а затем вызвать resume() из планировщика; минусом становится необходимость корректного владения дескриптором и обработки отмены.
Выбран std::suspend_always: он соответствует требованию отложенного запуска. Возвращаемый тип владеет кадром корутины, планировщик владеет задачей до её запуска, а после завершения кадр уничтожается владельцем. Это предотвращает как преждевременный сетевой запрос, так и утечку кадра.
Нет. При std::suspend_always в initial_suspend() тело может не выполниться вообще, если вызывающий код не вызовет resume(), а владелец задачи уничтожит кадр. Поэтому создание объекта задачи не следует трактовать как запуск операции.
resume() после завершения корутины?Нет. Если корутина уже достигла final_suspend, её дескриптор обозначает завершённую корутину. Повторный resume() для завершённой корутины приводит к неопределённому поведению. Перед возобновлением обычно проверяют handle.done(), а после завершения передают владение кадром его владельцу для вызова destroy().
initial_suspend() часто возвращает std::suspend_always в типах задач?Такой выбор отделяет создание задачи от её планирования. Тип задачи может сохранить дескриптор, зарегистрировать его в исполнительной системе, связать с отменой и только затем разрешить выполнение. Цена — необходимость определить правила владения, обработать исключения и гарантировать, что задача не будет уничтожена до запуска или завершения.