Разберите порядок запуска: что гарантирует async let до первого await и какой порядок вывода допустим?
func work(_ name: String) async -> String {
print("start \(name)")
await Task.yield()
print("finish \(name)")
return name
}
func demo() async {
async let first = work("A")
print("between")
async let second = work("B")
print("before await")
_ = await (first, second)
}
При выполнении объявления async let дочерняя задача создаётся сразу, а не в момент последующего await. Однако Swift не гарантирует, что она фактически успеет выполнить первую инструкцию до следующей строки родительской задачи, поэтому порядок вывода недетерминирован.
В примере first создаётся до print("between"), а second — до print("before await"). Обе операции могут выполняться конкурентно, но это не обещает ни конкретного порядка сообщений, ни выполнения на разных ядрах.
Структурированная конкурентность появилась как способ связать жизненный цикл дочерних задач с областью видимости родительского кода. Это уменьшает риск утечек задач, потери ошибок и забытых ожиданий по сравнению с ручным запуском независимых задач.
async let предназначен для фиксированного небольшого набора независимых асинхронных операций. Он позволяет выразить параллельное получение результатов, сохраняя автоматическое ожидание и управление дочерними задачами при выходе из области видимости.
Ошибочное предположение состоит в том, что async let ведёт себя как ленивое выражение: будто вызов work начнётся только при чтении first или second. При таком понимании разработчик может неверно оценить задержку, порядок побочных эффектов или ожидаемую степень конкуренции.
Обратная ошибка тоже опасна: создание async let не означает, что дочерняя задача немедленно получила процессор. Планировщик может сначала продолжить родительскую задачу, поэтому журналы и побочные эффекты нельзя синхронизировать одним фактом объявления async let.
Объявление async let first = work("A") создаёт структурированную дочернюю задачу и запускает вычисление work("A"). Родительская задача не ждёт её завершения и продолжает выполнение следующей инструкции.
После объявления second обе дочерние задачи могут прогрессировать независимо. await (first, second) получает их результаты; ожидание первой переменной не запускает вычисление, а лишь дожидается уже созданной задачи.
Гарантируется порядок создания: first создаётся раньше second. Не гарантируется порядок выполнения строк print, потому что родительская задача и дочерние задачи планируются конкурентно. Например, between может появиться до start A, хотя first уже создана.
async let остаётся структурированной задачей: при выходе из области, содержащей binding, Swift не оставляет её бесхозной. Незавершённое вычисление должно быть обработано до выхода; при необходимости оно отменяется, а область ожидает завершения дочерней задачи. Поэтому async let не подходит для фоновой работы, которая должна пережить родительскую область.
Конкурентное выполнение не равно гарантированному параллельному выполнению. Задачи могут чередоваться на одном потоке, выполняться на разных потоках или быть ограничены изоляцией одного актора. Кроме того, async let полезен только для независимых операций: если вторая операция зависит от результата первой, ранний запуск не даёт выигрыша.
Минимальная форма выглядит так:
Здесь оба вызова запускаются при создании binding, а результат возвращается после получения обоих значений. Если операции должны создаваться динамически или их число неизвестно заранее, вместо нескольких async let обычно выбирают TaskGroup.
Экран приложения должен одновременно загрузить профиль и настройки пользователя. Последовательный вариант сначала ждёт профиль, затем начинает загрузку настроек: он проще для зависимых операций, но суммарная задержка примерно складывается из двух задержек.
Ручной запуск двух Task может уменьшить задержку, однако разработчику придётся отдельно продумать отмену, обработку ошибок и момент ожидания. Это повышает риск оставить задачу выполняться после ухода со страницы.
Выбран async let, потому что число операций фиксировано, результаты независимы, а экран не должен владеть бесконтрольными фоновыми задачами. Загрузки стартуют при создании binding, область гарантированно дожидается их обработки, а код явно показывает зависимость между конкурентными операциями и итоговым await.
Можно ли по расположению объявления гарантировать, что start A появится раньше between?
Нет. Объявление async let гарантирует создание дочерней задачи до перехода к следующей инструкции, но не её немедленное фактическое выполнение. Планировщик может сначала продолжить родительскую задачу, поэтому between допустимо увидеть раньше start A.
Что произойдёт, если результат async let явно не прочитать?
Дочерняя задача всё равно не станет независимой фоновой задачей. При выходе из области Swift должен завершить обработку этого структурированного binding: незавершённую работу может потребоваться отменить, после чего область дождётся её завершения. Следовательно, неиспользуемый async let способен задержать выход из функции и обычно указывает на ошибку проектирования или лишнюю работу.
Гарантирует ли несколько async let выполнение на разных потоках или ядрах?
Нет. Они создают конкурентные дочерние задачи, но конкретное планирование остаётся за runtime и системным исполнителем. Задачи могут выполняться параллельно, чередоваться на одном потоке или последовательно проходить через одну actor-изоляцию; async let гарантирует структуру и возможность конкуренции, а не физический параллелизм.