Гарантирует ли атрибут `@MainActor` для класса или ViewModel, что весь его код выполнится на главном потоке?

Ответ

Нет, не весь код автоматически выполнится на главном потоке. @MainActor гарантирует, что вход в его неизолированные (nonisolated) свойства и методы будет происходить на главном потоке. Однако, внутри async-метода поток может смениться после точки await.

Проблема и решение:

@MainActor
class MyViewModel {
    var data: [String] = []

    func fetchAndUpdate() async {
        // 1. Эта точка выполняется на главном потоке (гарантия @MainActor).

        let newData = await networkService.fetchData() // 2. `await` -> приостановка.
        // 3. После возобновления поток МОЖЕТ БЫТЬ ЛЮБЫМ (например, фоновый).

        // ОПАСНО: Модификация состояния не на главном потоке.
        self.data = newData // Возможна гонка данных и краш UI.

        // РЕШЕНИЕ: Явное возвращение на главный актор.
        await MainActor.run {
            self.data = newData // Теперь безопасно.
        }
    }
}

Правила и лучшие практики:

  1. Гарантируется: Синхронный код и код до первого await в методе @MainActor-типа.
  2. Не гарантируется: Код после await. Исполнитель (Executor) может возобновить выполнение на другом потоке.
  3. Что делать: Все операции, связанные с обновлением состояния (@Published, @State, UI-методы), которые идут после await, нужно оборачивать в await MainActor.run { ... }.

Вывод: @MainActor обеспечивает изоляцию, но в асинхронном контексте ответственность за выполнение UI-кода на главном потоке после await лежит на разработчике.

Ответ 18+ 🔞

А, слушай, вот эта тема с @MainActor — она ж как ловушка для самоуверенных, блядь. Все думают: «А, хуле, поставил атрибут — и поехали, всё само на главном будет!». А потом — бац! — краш, гонка данных, UI обоссывается, и ты сидишь, чешешь репу, как дурак.

Вот смотри, в чём подвох, ёпта. @MainActor — он как охранник у входа в клуб. Гарантирует, что зайти ты сможешь только по главному пропуску. То есть вызов метода, обращение к свойству — это всё будет на главном потоке. Это железно.

Но внутри-то, сука, клуб асинхронный! Там свои комнаты, коридоры. Ты зашёл, сказал «await» — и тебя, блядь, могут вывести в подсобку на другую смену! После того как ты «проснёшься» от этого await, исполнитель может быть уже какой угодно, хоть фоновый, хоть из параллельной вселенной.

Вот тебе наглядный пиздец:

@MainActor
class MyViewModel {
    var data: [String] = [] // @Published, например

    func fetchAndUpdate() async {
        // 1. Тут ты на главном. Всё чётко. @MainActor рулит.

        let newData = await networkService.fetchData() // 2. Сказал "await" — уснул.
        // 3. Проснулся. А где проснулся? А хрен его знает! Может, уже не на главном!

        // ОПАСНОСТЬ, БЛЯДЬ! Модификация UI-состояния не на главном потоке!
        self.data = newData // Вот тут и выстрелит в ногу. Гонка. Краш. Пиздец.

        // СПАСЕНИЕ: Явно зовём главного актора обратно, как такси.
        await MainActor.run {
            self.data = newData // А вот теперь — да, безопасно, мы дома.
        }
    }
}

Краткие правила, чтобы не облажаться:

  1. Гарантировано: Весь синхронный код в методе и всё до первого await — будет на главном потоке. Это закон.
  2. НЕ гарантировано нихуя: Всё, что после await. После паузы ты можешь очнуться где угодно. Это как после хорошей пьянки — просыпаешься не всегда в своей кровати.
  3. Что делать, мудя: Любое обновление @Published, @State, вызов UIKita или свифтUI-шного метода — которое идёт после await — нужно вручную заворачивать в await MainActor.run { ... }. Это твой спасательный круг.

Вывод, ёбана: @MainActor — это не магическая таблетка «сделай всё на мейне». Это изоляция входа. А в асинхронном мире, после каждой точки await, ты сам должен следить, чтобы твой UI-код возвращался на главную сцену. Иначе — чих-пых тебя в сраку с твоими крешами.