Объясните, почему вызов async функции сам по себе не запускает новую задачу.

Объясните, почему вызов async-функции сам по себе не запускает новую задачу.

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

async-функция не создаёт новую задачу: она выполняется внутри уже существующей задачи и сохраняет её контекст, пока не достигнет точки приостановки. Чтобы начать независимое конкурентное выполнение, нужно явно создать задачу через Task, async let или TaskGroup.

Ключевое различие такое: async описывает возможность приостанавливать выполнение, а Task — единицу выполняемой работы и её жизненный цикл.

Исторический контекст

До появления async/await асинхронность обычно строилась на callback-функциях, ручном управлении очередями и явном создании потоков. Это усложняло чтение кода и часто приводило к ошибкам при передаче результатов, обработке отмены и сохранении контекста выполнения.

Модель Swift Concurrency разделяет две задачи: async/await делает последовательный код способным приостанавливаться, а структурированные примитивы конкурентности управляют параллельным запуском, завершением и отменой работ.

Постановка проблемы

Если разработчик считает каждый вызов async-функции отдельной задачей, он может ошибочно ожидать параллельного выполнения. Последовательные вызовы в одной задаче будут выполняться один за другим, даже если каждая функция объявлена как async.

Это приводит к лишнему времени ожидания, неверным выводам о порядке операций и ошибочному использованию async/await как замены механизмам запуска конкурентной работы. При этом создание независимых задач без необходимости тоже опасно: усложняется управление их отменой и жизненным циклом.

Подробное решение

При вызове async-функции текущая задача передаёт ей управление. Функция выполняется синхронно до первой точки приостановки или до завершения. Если результат ещё не готов, задача приостанавливается, а поток не обязан простаивать: исполнитель может использовать его для другой готовой задачи.

После возобновления функция продолжает работу в рамках той же задачи, но не обязана продолжить выполнение на том же физическом потоке. Само наличие async не означает ни создания потока, ни автоматического запуска параллельной работы.

func fetch(_ key: String) async -> String { return key } func sequential() async -> (String, String) { let first = await fetch("first") let second = await fetch("second") return (first, second) } func concurrent() async -> (String, String) { async let first = fetch("first") async let second = fetch("second") return await (first, second) }

В sequential второй вызов начинается только после получения результата первого. В concurrent два дочерних вычисления запускаются как части структурированной задачи, поэтому их работа может перекрываться.

Task создаёт отдельную, неявно структурированную задачу. async let и TaskGroup создают дочерние задачи с более строгим контролем области видимости: родитель должен дождаться их завершения или обработать их отмену перед выходом из соответствующей области.

Асинхронность и параллельность также не тождественны. Async-функция может только приостанавливать выполнение, но фактическая конкурентность зависит от того, какие задачи запущены, где находятся точки приостановки и какие ограничения накладывает исполнитель, например изоляция actor.

Ситуация из практики

Экран должен загрузить профиль пользователя и список уведомлений. Разработчик написал два последовательных вызова async-функций, поэтому список уведомлений начинает загружаться только после завершения загрузки профиля.

Рассматривались два варианта:

  • создать две независимые Task — это даёт конкурентный запуск, но требует отдельно продумывать отмену, хранение дескрипторов и обработку жизненного цикла;
  • использовать async let — дочерние задачи автоматически связаны с родительской областью, а результаты удобно собрать в конце.

Выбран async let, поскольку обе операции нужны для формирования одного результата и имеют одинаковый жизненный цикл. Загрузка стала конкурентной, а дочерние задачи не остались выполняться после выхода родительской функции.

Если же одна операция должна продолжать работу независимо от вызывающей функции, например отправка аналитического события, уместнее отдельная Task с явно определённой политикой хранения и отмены. Это уже осознанная потеря части структурированного контроля.

Что кандидаты часто упускают

  1. Может ли async-функция выполняться на другом потоке, хотя новая задача не создавалась?

Да. После приостановки продолжение задачи может быть запланировано на другом потоке, если это допускает используемый исполнитель. Это не означает создания новой задачи: задача остаётся той же, меняется только поток, на котором исполняется её очередной фрагмент.

Поэтому код не должен полагаться на сохранение потока после await, если это отдельно не гарантируется изоляцией, например MainActor. Поток и задача — разные сущности.

  1. Будут ли два последовательных вызова async-функции выполняться параллельно, если первый содержит await?

Нет. Вторая строка начнёт выполняться только после того, как первая async-функция вернёт результат. Приостановка первой задачи освобождает поток, но не передаёт автоматически выполнение второй операции.

Параллельный запуск должен быть выражен явно: через async let, группу задач или отдельные Task. Само ожидание результата не превращает последующие операции в конкурентные.

  1. Зачем создавать Task, если async-функцию можно просто вызвать с await?

Прямой вызов с await подходит, когда вызывающий код должен дождаться результата в рамках текущей задачи. Task нужна, когда работу требуется запустить независимо от текущего последовательного шага, сохранить её дескриптор, отменить или наблюдать за её результатом отдельно.

Однако отдельная Task увеличивает ответственность разработчика: нужно определить, кто владеет её жизненным циклом, что происходит при уходе владельца со страницы и как обрабатываются ошибки. Если работа логически принадлежит текущей операции, предпочтительнее структурированные примитивы, а не неструктурированная Task.