Программирование SwiftКонкурентностьРазработчик приложений на Swift

Какова граница действия defer в async функции, если выполнение проходит через await?

Какова граница действия defer в async-функции, если выполнение проходит через await?

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

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

defer не выполняется в момент await и не привязан к приостановке задачи. Он срабатывает только при выходе из лексической области, в которой был объявлен: при обычном возврате, выбросе ошибки или фактическом разматывании стека.

Поэтому defer сохраняет зарегистрированное действие во время приостановки async-функции и выполнит его после возобновления, когда соответствующая область завершится.

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

defer предназначен для гарантированного освобождения ресурсов при выходе из области видимости. С появлением async/await важно отличать логическую границу области кода от временной приостановки выполнения: await не завершает функцию и не покидает её локальные области.

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

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

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

Отмена задачи сама по себе также не запускает defer: отмена в Swift кооперативна. Блок выполнится, только если функция обнаружит отмену и завершится либо завершится по другой причине.

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

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

func operation() async { defer { print("exit") } print("before") await Task.yield() print("after") }

При завершении функции вывод будет происходить в порядке before, after, exit. Даже если Task.yield() действительно передаст управление другой задаче, defer не сработает между before и after.

Область действия имеет лексическую природу. Если defer объявлен во вложенном блоке, он выполнится при выходе именно из этого блока, даже если внутри блока был await. Несколько defer в одной области выполняются в обратном порядке регистрации.

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

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

Сервис начинает трассировочный интервал перед запросом к серверу и регистрирует defer для его завершения. Если внутри функции после запроса выполняется ещё одно длительное await, интервал будет включать оба этапа, а не только сетевой запрос.

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

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

  1. Выполнится ли defer, если задача отменили во время await?

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

  2. Что произойдёт с defer, объявленным во вложенной области до await?

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

  3. В каком порядке выполняются несколько defer при ошибке?

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