В каких условиях вызов MainActor.assumeIsolated из синхронного кода приводит к аварийному завершению?
Вызов MainActor.assumeIsolated приводит к аварийному завершению, если в момент вызова код фактически не выполняется в изоляции MainActor. Метод не переносит выполнение на главный актор, а лишь проверяет уже существующее предположение об изоляции.
Если предположение верно, переданное замыкание выполняется синхронно. Если нужен безопасный переход с произвольного контекста, следует использовать асинхронный hop, например через Task { @MainActor in ... }, а не assumeIsolated.
Actors и глобальные акторы в Swift решают проблему конкурентного доступа к изменяемому состоянию: компилятор проверяет, из какого контекста разрешено обращаться к изолированным данным.
Однако существуют синхронные callback-интерфейсы и legacy API, где разработчик внешне знает, что callback всегда вызывается на главном потоке, но типовая система не может выразить эту гарантию. MainActor.assumeIsolated предназначен для такого узкого взаимодействия: он позволяет синхронно сообщить компилятору и среде выполнения, что изоляция уже обеспечена вызывающей стороной.
Синхронный не изолированный callback не может произвольно обращаться к состоянию объекта, изолированного MainActor. Простое утверждение разработчика о том, что callback «обычно приходит с главного потока», недостаточно: при нарушении этого условия возникает гонка данных или некорректный доступ к состоянию.
Неверное применение assumeIsolated особенно опасно тем, что оно выглядит как безопасное получение доступа, хотя на самом деле не выполняет переключение контекста. При вызове с фонового исполнителя программа завершится аварийно вместо автоматического перехода на главный актор.
MainActor.assumeIsolated проверяет, что текущий код уже выполняется в контексте MainActor. При успешной проверке замыкание считается изолированным главным актором и выполняется немедленно, без создания задачи, ожидания или планирования перехода.
При вызове из неподходящего контекста срабатывает проверка предусловия, поэтому результатом становится runtime failure, обычно аварийное завершение процесса. Это защитнее, чем молча выполнить доступ к состоянию с неправильного исполнителя, но не заменяет синхронизацию.
Минимальный пример:
Этот код корректен только при документированной гарантии, что legacyCallback всегда вызывается в изоляции MainActor. Если callback может прийти с любого потока, безопаснее запланировать работу на главном акторе асинхронно; это добавит hop и изменит момент обновления состояния, зато не потребует недоказуемого предположения.
assumeIsolated также не делает произвольные захваченные объекты потокобезопасными и не защищает состояние, которое находится за пределами изоляции MainActor. Он только использует гарантию текущего контекста для доступа к соответствующему изолированному коду.
Предположим, системный делегат документирует вызов метода только на главном потоке, но его Swift-объявление остаётся синхронным и не помечено @MainActor. Внутри метода нужно немедленно обновить состояние модели, изолированной главным актором.
Вариант с Task { @MainActor in ... } безопаснее при неопределённом источнике callback: он выполняет обновление в правильном контексте. Минус — обновление становится асинхронным, callback может завершиться раньше, а состояние к моменту чтения может ещё не обновиться.
Вариант с MainActor.assumeIsolated сохраняет синхронную семантику и не создаёт дополнительную задачу. Его выбирают только при устойчивой внешней гарантии главного контекста; если библиотека нарушит контракт или callback начнёт вызываться в фоне, приложение аварийно завершится. В результате решение должно быть основано не на удобстве, а на проверяемом контракте API.
Чем MainActor.assumeIsolated отличается от MainActor.run?
MainActor.run — асинхронный механизм, который при необходимости планирует выполнение замыкания на MainActor и требует await. assumeIsolated не выполняет переход: он проверяет текущий контекст и при успехе запускает замыкание синхронно. Поэтому run подходит для произвольного вызывающего контекста, а assumeIsolated — только для доказанно изолированного.
Можно ли использовать assumeIsolated, чтобы отключить предупреждение компилятора о конкурентном доступе?
Нет. Это не универсальное подавление проверки и не способ сделать небезопасный объект потокобезопасным. API допустим лишь тогда, когда вызывающая сторона действительно гарантирует выполнение в изоляции нужного глобального актора; иначе проверка лишь переносит обнаружение ошибки с компиляции на выполнение.
Что произойдёт, если callback иногда вызывается на MainActor, а иногда на фоновом исполнителе?
Поведение станет зависеть от конкретного вызова: корректные вызовы пройдут, а фоновые приведут к аварийному завершению при проверке изоляции. В такой ситуации нельзя использовать assumeIsolated; нужно всегда выполнять обновление через асинхронный переход на MainActor либо синхронно обеспечить единый контекст до вызова callback.