Представьте не изолированную async-функцию, начавшуюся на главном потоке: обязана ли она продолжить выполнение после await на том же потоке?
Нет. Неизолированная async-функция не обязана продолжать выполнение на том же потоке после await: поток может измениться даже внутри одной задачи. Гарантию даёт не async/await, а изоляция конкретным actor или MainActor.
То, что выполнение иногда возвращается на прежний поток, является допустимым совпадением, а не контрактом. Код, которому нужна привязка к главному потоку, должен быть изолирован MainActor.
Традиционная модель с ручным управлением потоками заставляла разработчика самостоятельно создавать потоки, синхронизировать доступ к данным и решать, где продолжать работу после блокирующего ожидания. Это приводило к простою потоков, гонкам данных и сложным цепочкам обратных вызовов.
async/await отделяет логическую задачу от конкретного потока. Во время await задача может приостановиться, а поток — выполнять другую работу; после готовности результата задача планируется снова на подходящем executor-е.
Разработчик может ошибочно считать, что вызов async-функции с главного потока сохраняет эту привязку на всём протяжении операции. Тогда после await в неизолированной функции начинают напрямую использоваться объекты пользовательского интерфейса или разделяемое состояние.
Последствие — нарушение требований UI-фреймворка или гонка данных. Обратная ошибка тоже возможна: тяжёлую работу считают гарантированно фоновой только потому, что она объявлена как async, хотя сама по себе асинхронность не означает отдельный поток и не гарантирует конкретный способ планирования.
async описывает возможность приостановить функцию, но не закрепляет её за потоком. В точке await текущая задача освобождает поток; когда ожидаемая операция завершается, продолжение задачи ставится на выполнение согласно её изоляции и правилам планировщика.
Если функция не изолирована actor-ом, после возобновления у неё нет гарантии прежнего потока. Важно мыслить не термином «поток задачи», а сочетанием задача + executor + изоляция: одна задача может последовательно выполняться на разных потоках.
Изоляция MainActor означает, что доступ к изолированному состоянию выполняется через главный actor. Поэтому UI-состояние следует хранить и изменять в коде, изолированном MainActor, а не полагаться на поток, с которого когда-то начался вызов.
Неизолированную вычислительную работу можно выполнять без MainActor, но это не равно гарантии «всегда фоновый поток». Если требуется явно отделить работу от контекста вызывающего кода, нужно выбирать подходящий дизайн: отдельный actor, структурированную дочернюю задачу или, в особых случаях, Task.detached. Последний вариант требует самостоятельного контроля наследования контекста, отмены и передачи только безопасных для конкурентности значений.
Минимальный пример показывает различие между изоляцией UI и неизолированной async-функцией:
loadValue не обещает продолжение на главном потоке. Замыкание Task создано внутри MainActor-изолированного метода, поэтому присваивание title выполняется в контексте MainActor; безопасность UI не зависит от потока, на котором работала loadValue.
Экран приложения запускает загрузку и последующую обработку большого ответа. Первая реализация помечает всю операцию @MainActor, что упрощает доступ к состоянию экрана, но может перегрузить главный executor вычислениями и ухудшить отзывчивость интерфейса.
Второй вариант снимает изоляцию со всей функции и после await напрямую меняет UI. Он лучше отделяет вычисления, но небезопасен: отсутствие MainActor не даёт права обращаться к UI из произвольного контекста.
Практичное решение — разделить ответственность. Загрузку и преобразование данных оставить неизолированными, а обновление состояния экрана выполнить в MainActor-изолированном методе или типе. Такой вариант сохраняет отзывчивость интерфейса и явно фиксирует границу, на которой данные попадают в UI.
Обязательно ли после await происходить переключение потока?
Нет. await обозначает потенциальную точку приостановки, но задача может продолжить выполнение сразу или на том же потоке. Контракт гарантирует возможность уступить выполнение, а не обязательный thread hop.
Гарантирует ли MainActor, что каждый машинный вызов физически выполнится на одном заранее закреплённом потоке?
Нет, правильная гарантия формулируется через executor и изоляцию MainActor, а не через ручную привязку к идентификатору потока. Для обычного приложения MainActor связан с главным контекстом выполнения UI, но переносить на это утверждение требования низкоуровневого thread affinity без специального API нельзя.
Безопасно ли читать объект UI после await, если функция началась на главном потоке?
Нет, самого факта начального вызова недостаточно. После приостановки неизолированная функция может возобновиться вне главного actor-а, поэтому обращение к UI должно происходить в MainActor-изолированном коде. Передача результата из фоновой части и последующее обновление UI на MainActor безопаснее, чем сохранение предположения о потоке.