Async/await и структурированная конкурентность
Как `await` приостанавливает без блокировки, структурированные и неструктурированные задачи, task group'ы, кооперативная отмена, continuation'ы и async-последовательности.
13 вопросов
JuniorТеорияОчень частоЧто делают async/await и чем они отличаются от completion handler?
Что делают async/await и чем они отличаются от completion handler?
async-функция приостанавливается и возобновляется, не блокируя поток; await отмечает ожидание результата. В отличие от completion handler, код читается сверху вниз, возвращает значения и бросает ошибки через throw.
Типичные ошибки
- ✗Считают, что
awaitблокирует вызывающий поток на время ожидания - ✗Думают, что любой
async-вызов уходит на новый фоновый поток - ✗Полагают, что ошибки всё равно доставляются колбэком, а не через
throw
Уточняющие вопросы
- →На каком потоке
async-функция возобновляется послеawait? - →Как throwing
async-функция сообщает вызывающему об ошибке?
MiddleКодОчень частоЗагрузите три эндпоинта через async let, затем N элементов через TaskGroup
Загрузите три эндпоинта через async let, затем N элементов через TaskGroup
Свяжите три известных эндпоинта через async let и прочитайте одним общим await — они идут параллельно. Для динамического N возьмите throwing task group: addTask на элемент, соберите через for try await с пробросом ошибки задачи.
Типичные ошибки
- ✗Ждут каждый
async letотдельно, делая загрузки последовательными - ✗Думают, что
TaskGroupработает только с фиксированным списком задач - ✗Пишут результаты группы в общий массив вместо возврата их из задачи
Уточняющие вопросы
- →Почему привязка через
async letстартует работу доawait? - →Как throwing task group пробрасывает ошибку одной дочерней задачи родителю?
MiddleТеорияЧастоasync let против TaskGroup против трёх последовательных await — что реально работает параллельно?
async let против TaskGroup против трёх последовательных await — что реально работает параллельно?
Три последовательных await идут один за другим — без параллелизма. async let запускает фиксированный, известный набор дочерних задач сразу, ожидаемых вместе. TaskGroup запускает динамическое число для N элементов.
Типичные ошибки
- ✗Думают, что
async letждёт каждую привязку до старта следующей - ✗Считают, что последовательные
awaitперекрываются, раз каждый приостанавливается - ✗Путают, какая конструкция для фиксированного, а какая для динамического числа задач
Уточняющие вопросы
- →Когда дочерняя задача
async letфактически начинает выполняться? - →Почему результаты дочерних задач
TaskGroupнужно собрать до выхода из группы?
MiddleТеорияЧастоЧто такое точка приостановки и почему await не блокирует поток, на котором выполняется?
Что такое точка приостановки и почему await не блокирует поток, на котором выполняется?
await — потенциальная точка приостановки: функция сохраняет состояние, отдаёт поток в пул и может возобновиться на другом потоке. Поток идёт на другую работу, поэтому приостановка не блокирует поток.
Типичные ошибки
- ✗Думают, что
awaitпаркует и блокирует вызывающий поток - ✗Считают, что задача всегда возобновляется на том же потоке, где приостановилась
- ✗Полагают, что каждая приостановка создаёт или занимает OS-поток
Уточняющие вопросы
- →Какие гарантии даёт Swift о том, на каком потоке задача возобновится?
- →Как кооперативный пул потоков избегает взрывного роста потоков
DispatchQueue?
MiddleКодЧастоОтмените текущий поиск при смене запроса — двумя способами
Отмените текущий поиск при смене запроса — двумя способами
Храните поиск в Task?; при новом запросе — cancel() перед следующим, чтобы старый запрос остановился. В SwiftUI то же — .task(id: query) при смене query. Fetch должен проверять отмену, чтобы остановиться.
Типичные ошибки
- ✗Считают, что сброс ссылки на
Taskотменяет её - ✗Думают, что
cancel()обрывает сетевой вызов без проверки - ✗Полагают, что
.task(id:)не реагирует на смену значенияid
Уточняющие вопросы
- →Почему сама функция поиска должна проверять
Task.isCancelled? - →Как
.task(id:)решает, когда перезапустить своё async-тело?
MiddleКодЧастоОберните API с completion handler в async через checked continuation
Оберните API с completion handler в async через checked continuation
withCheckedThrowingContinuation даёт continuation; зовите resume один раз — с результатом или ошибкой. Повторный resume приводит к аварии, ведь на втором вызове он падает; без resume задача виснет навсегда.
Типичные ошибки
- ✗Думают, что continuation можно возобновлять больше одного раза
- ✗Считают, что забытый
resumeуберётся сам, а не подвесит задачу - ✗Путают checked (падает) с unsafe (неопределённое поведение)
Уточняющие вопросы
- →Что checked-вариант
withCheckedContinuationпроверяет во время выполнения? - →Как защитить continuation, если колбэк может сработать дважды?
MiddleТеорияЧастоПочему отмена задач в Swift кооперативная и что ломается, если её игнорировать?
Почему отмена задач в Swift кооперативная и что ломается, если её игнорировать?
Отмена лишь выставляет флаг и не останавливает задачу принудительно. Задача должна проверять Task.isCancelled или try Task.checkCancellation() и выходить раньше. Иначе работа доходит до конца, тратя ресурсы на ненужный результат.
Типичные ошибки
- ✗Ждут, что отмена вытеснит и мгновенно убьёт задачу
- ✗Считают, что рантайм останавливает отменённую работу без проверок
- ✗Думают, что отменённая работа отбрасывается, а не доходит до конца
Уточняющие вопросы
- →Где длинный вычислительный цикл должен ставить проверки отмены?
- →Что происходит с дочерними задачами при отмене родительской?
MiddleТеорияЧастоTask {} против Task.detached против дочерней задачи — что наследует каждая?
Task {} против Task.detached против дочерней задачи — что наследует каждая?
Структурированная дочерняя задача наследует приоритет, actor и task-locals, ограничена родителем. Неструктурированная Task {} всё равно наследует приоритет, actor и task-locals. Task.detached не наследует ничего и работает независимо.
Типичные ошибки
- ✗Думают, что
Task {}не наследует ничего, какTask.detached - ✗Считают, что
Task.detachedпереносит текущий actor - ✗Полагают, что task-local-значения попадают в detached-задачу
Уточняющие вопросы
- →Почему
Task.detachedнамеренно отбрасывает окружающий контекст актора? - →Когда неструктурированной
Task {}нужна ручная отмена?
MiddleДебаггингЧастоTask {} из viewDidLoad продолжает работать после закрытия экрана и удерживает self
Task {} из viewDidLoad продолжает работать после закрытия экрана и удерживает self
Две ошибки. Неструктурированная Task {} не привязана к контроллеру — закрытие её не отменяет, и она работает дальше. И она сильно захватывает self, блокируя deinit. Фикс — сохраните задачу, cancel() при уходе, захват [weak self].
Типичные ошибки
- ✗Думают, что неструктурированная
Task {}отменяется при deinit - ✗Считают, что
Task {}ожидают как структурированную дочернюю задачу - ✗Ждут, что
Task.detachedразом решит время жизни и захват
Уточняющие вопросы
- →Почему сильный захват
selfдержит контроллер живым? - →Где в жизненном цикле UIKit нужно отменять хранимую задачу?
MiddleТеорияИногдаКак превратить поток из делегата или колбэков в AsyncStream, по которому можно for await?
Как превратить поток из делегата или колбэков в AsyncStream, по которому можно for await?
Оберните источник в AsyncStream { continuation in ... }: yield(value) на каждый колбэк, finish() при завершении, onTermination для отписки. Потребители for await value in stream — он выдаёт много значений, а не одно.
Типичные ошибки
- ✗Берут одноразовый continuation вместо
AsyncStream - ✗Забывают
finish(), поэтому потребителиfor awaitнавсегда - ✗Пропускают
onTermination, оставляя подписку источника
Уточняющие вопросы
- →Что
onTerminationпозволяет освободить, когда итерация прекращается? - →Как политика буферизации
AsyncStreamсправляется с быстрым источником?
SeniorКодИногдаОграничьте число одновременных загрузок до N через task group
Ограничьте число одновременных загрузок до N через task group
Заполните группу первыми N задачами; когда результат приходит, добавляйте следующий элемент. Так в работе всегда N — не более N дочерних задач — что ограничивает память и число соединений. Добавите все задачи сразу — запустятся все разом.
Открыть задачу →Типичные ошибки
- ✗Считают, что task group сам ограничивается числом ядер
- ✗Блокируют дочерние задачи семафором вместо дросселирования добавлений
- ✗Думают, что нельзя управлять числом одновременных задач группы
Уточняющие вопросы
- →Что происходит с остатком очереди, если одна дочерняя задача бросает ошибку?
- →Как сохранить порядок входа, ограничивая параллелизм числом N?
SeniorДизайнИногдаВам достаётся средних размеров приложение, чьи сетевой слой и слой данных целиком построены на dispatch-фреймворке GCD — переходы между DispatchQueue, колбэки completion handler и несколько ожиданий DispatchSemaphore — и его нужно перевести на async/await постепенно, без переписывания разом и продолжая выпускать фичи каждый спринт. Опишите порядок миграции: какой слой переводите первым, как мостите ещё не переведённые границы в обе стороны (вызов старых callback-API из нового async-кода и вызов async из старого), что даёт обёртка withCheckedContinuation на стыках и что ломается первым — скрытые предположения о потоках, обновления UI на @MainActor или дедлоки на семафоре, когда async-вызов заперт за блокирующим ожиданием. Какой порядок оставляет приложение готовым к релизу на каждом шаге?
Вам достаётся средних размеров приложение, чьи сетевой слой и слой данных целиком построены на dispatch-фреймворке GCD — переходы между DispatchQueue, колбэки completion handler и несколько ожиданий DispatchSemaphore — и его нужно перевести на async/await постепенно, без переписывания разом и продолжая выпускать фичи каждый спринт. Опишите порядок миграции: какой слой переводите первым, как мостите ещё не переведённые границы в обе стороны (вызов старых callback-API из нового async-кода и вызов async из старого), что даёт обёртка withCheckedContinuation на стыках и что ломается первым — скрытые предположения о потоках, обновления UI на @MainActor или дедлоки на семафоре, когда async-вызов заперт за блокирующим ожиданием. Какой порядок оставляет приложение готовым к релизу на каждом шаге?
Снизу вверх — оберните нижние callback-API в withCheckedContinuation, чтобы верхние слои шли на async. Мостите обе стороны: continuation для callback→async, Task {} — обратно. Сначала leaf-сервисы, UI последним. Ожидания на семафоре ломаются первыми — дедлок пула.
Типичные ошибки
- ✗Мигрируют сверху вниз (сначала UI) вместо снизу вверх от нижних API
- ✗Оставляют ожидания
DispatchSemaphore, блокирующие кооперативный пул - ✗Ждут, что
Task.detachedсмостит колбэки без continuation
Уточняющие вопросы
- →Как держать приложение готовым к релизу в ходе многоспринтовой миграции?
- →Почему блокирующее ожидание внутри
async-функции рискует дедлоком?
SeniorТеорияИногдаКакую задачу решают task-local-значения и как они ведут себя через Task.detached?
Какую задачу решают task-local-значения и как они ведут себя через Task.detached?
@TaskLocal переносит окружающий контекст — request id, trace span — вниз по дереву вызовов, а не через сигнатуры. Заданное withValue { }, оно наследуется дочерними задачами и Task {}; Task.detached не наследует ничего и читает значение по умолчанию.
Типичные ошибки
- ✗Считают
@TaskLocalизменяемой глобальной переменной - ✗Полагают, что
Task.detachedнаследует task-local вызывающего - ✗Путают область task-local с thread-local-хранилищем
Уточняющие вопросы
- →Как заново установить trace id внутри
Task.detached? - →Почему область
withValueбезопаснее изменяемой глобали для контекста?