@MainActor — что это, применение и особенности в асинхронном коде Swift

Автор: IT Sectr Опубликовано: 2026-03-19 Время чтения: 8 мин

@MainActor — это глобальный актор в языке Swift, который гарантирует выполнение кода на главном потоке. По данным Apple Developer, 2024, @MainActor автоматизирует переключение на главный поток при работе с UI, избавляя разработчика от ручного вызова DispatchQueue.main.async. Аннотация появилась в Swift 5.5 вместе с системой async/await.

Главное

  • @MainActor — глобальный актор Swift для гарантии выполнения на главном потоке.
  • Система async/await — основа, на которой построена работа @MainActor.
  • Аннотация класса автоматически помещает все его методы на главный поток.
  • В отличие от DispatchQueue.main, @MainActor проверяет поток на уровне компилятора.
  • UI-обновления — основная область применения @MainActor в iOS-разработке.

Что такое @MainActor?

@MainActor — это глобальный актор (global actor) в Swift, который объединяет свойства акторов и гарантию выполнения на главном потоке приложения. Он является частью системы параллелизма Swift, представленной в Swift 5.5 вместе с async/await и структурированной конкурентностью. Аннотация позволяет разработчику не думать о переключении потоков вручную и снижает количество ошибок UI.

Определение и место в Swift Concurrency

Актор в Swift — это ссылочный тип, изолирующий своё состояние и гарантирующий, что только один поток может его изменять. @MainActor — специальный глобальный актор, исполнителем которого выступает главный поток. Любой код, помеченный @MainActor, выполняется на главном потоке — даже если он вызван из фоновой задачи.

До появления @MainActor разработчики вручную переключались на главный поток через DispatchQueue.main.async. Это был источник частых ошибок: разработчики забывали переключаться, что приводило к крашам из-за обновления UI не в главном потоке. @MainActor решает эту проблему на уровне системы типов.

Предпосылки создания

Источником большинства багов в iOS-приложениях является UI-небезопасность — обновление интерфейса из фонового потока. Apple встроила @MainActor в Swift Concurrency, чтобы сделать переключение на главный поток автоматическим и проверяемым компилятором, устранив целый класс runtime-ошибок.

Как работает @MainActor?

Принцип работы @MainActor основан на системе исполнения Swift Concurrency. Когда поток вызывает функцию, помеченную @MainActor, планировщик приостанавливает её на текущем исполнителе и возобновляет на главном потоке. Компилятор отслеживает границы вызова и гарантирует безопасность.

Исполнитель главного потока

За выполнение @MainActor отвечает MainActor.shared — исполнитель, ассоциированный с основным потоком приложения. Когда асинхронная функция помечена @MainActor, она всегда возобновляется на этом исполнителе, независимо от того, на каком потоке была запущена исходная задача.

swift
import SwiftUI

class ViewModel: ObservableObject {
    @Published var items: [String] = []

    @MainActor
    func loadData() async {
        let result = await fetchRemoteData()
        items = result  // безопасно, MainActor гарантирует главный поток
    }
}

Наследование контекста актора

Если функция помечена @MainActor и вызывает другую асинхронную функцию, по умолчанию она наследует контекст актора. Это означает, что все вложенные вызовы также выполняются на главном потоке, если не указано иное. Компилятор отслеживает это и выдаёт ошибку при попытке передать несогласованное замыкание.

@MainActor vs DispatchQueue.main

Сравнение @MainActor и DispatchQueue.main помогает понять, почему новый механизм считается более безопасным и удобным, хотя оба решают одну задачу — выполнение кода на главном потоке.

Безопасность на уровне типов

@MainActor — это проверка на уровне компилятора. Если вы пытаетесь вызвать @MainActor-функцию из небезопасного контекста, компилятор выдаст предупреждение или ошибку. DispatchQueue.main.async — это runtime-вызов: код скомпилируется, но может упасть в рантайме при попытке обновить UI из фонового потока.

Производительность и избыточность

DispatchQueue.main.async добавляет в очередь блок, который может быть выполнен с задержкой. @MainActor с async/await выполняет прямое переключение исполнителя без создания лишних замыканий. Это снижает накладные расходы и делает код более предсказуемым по времени выполнения.

swift
// Старый подход
DispatchQueue.main.async {
    self.updateUI()
}

// Новый подход с @MainActor
@MainActor
func updateUI() {
    // выполняется на главном потоке
    self.label.text = "Обновлено"
}
Критерий@MainActorDispatchQueue.main
Проверкакомпиляторruntime
Синтаксисаннотация (декларативный)вызов (императивный)
Накладные расходынизкие (переключение исполнителя)средние (замыкание + очередь)
Тестируемостьвысокая (MainActor.shared можно подменить)низкая (трудно мокировать)

Использование @MainActor в iOS-проектах

В реальных iOS-проектах @MainActor применяется в ViewModel-слоях, SwiftUI-вью и UIKit-контроллерах. Аннотация может быть применена как к отдельным методам, так и ко всему типу целиком.

Аннотация класса или структуры

Пометив класс @MainActor, вы гарантируете, что все его методы и свойства доступны только на главном потоке. Это особенно удобно для SwiftUI-вью и ObservableObject-классов: вы просто добавляете @MainActor перед class, и все @Published-свойства обновляются безопасно.

swift
@MainActor
final class UserListViewModel: ObservableObject {
    @Published var users: [User] = []
    @Published var isLoading = false

    func fetchUsers() async {
        isLoading = true
        users = await api.getUsers()
        isLoading = false
    }
}

Обёртывание legacy-кода

При работе со старым кодом на UIKit, где переключение потоков было ручным, можно использовать MainActor.run для явного переключения. Это удобно для инкрементального перехода на Swift Concurrency без переписывания всей кодовой базы.

swift
await MainActor.run {
    self.tableView.reloadData()
}

Ограничения @MainActor

Несмотря на все преимущества, @MainActor имеет ряд ограничений, которые важно учитывать при проектировании архитектуры приложения. Понимание границ применимости помогает избежать некорректного использования.

Производительность при интенсивном использовании

Если вся цепочка вызовов помечена @MainActor, то любая тяжёлая работа будет выполняться на главном потоке, вызывая подвисания UI. Рекомендуется помечать @MainActor только UI-слой, а бизнес-логику и сетевые запросы оставлять в фоновых акторах или глобальном исполнителе.

Несовместимость с некоторыми API

Старые callback-based API (например, URLSession без async/await) не поддерживают контекст актора. Для интеграции требуется обёртка с CheckedContinuation. Также @MainActor не совместим с performSelector, target-action и другими неасинхронными паттернами UIKit.

Многопоточная отладка

При отладке приложений с @MainActor сложнее воспроизвести состояния гонки, так как компилятор предотвращает многие из них на этапе сборки, а не в рантайме. Однако это может создать ложное чувство безопасности: неправильная работа с разделяемыми mutable-объектами (например, NSCache или общие глобальные переменные) всё ещё возможна, если они не помечены @MainActor и используются без явной синхронизации.

Тестирование @MainActor

@MainActor значительно упрощает тестирование UI-логики, так как избавляет от необходимости вручную переключать потоки в тестах. Однако существуют особенности, которые нужно учитывать при написании unit-тестов и UI-тестов.

Unit-тесты с MainActor

В XCTest среда выполнения автоматически настраивает исполнитель главного потока. Когда тестовый метод запускается на главном потоке, вызов @MainActor-функций не требует дополнительных настроек — они выполняются в том же контексте. Для тестирования фоновых сценариев используйте MainActor.run внутри Task с явным указанием приоритета и исполнителя, отдельно проверяя, что код корректно работает при вызове из фона.

Один из распространённых подходов — тестирование ViewModel с @MainActor, где проверяется, что @Published-свойства правильно обновляются после асинхронных операций. Благодаря наследованию контекста актора, вызов await внутри теста гарантирует выполнение на главном потоке без дополнительных DispatchQueue гарантий и ручного переключения контекстов, что упрощает написание тестов.

Проверка изоляции при рефакторинге

При рефакторинге существующего кода на Swift Concurrency проверяйте изоляцию @MainActor через компилятор: любые вызовы синхронных методов без @MainActor из контекста @MainActor помечаются как ошибка. Это свойство используется для постепенного перевода проекта на async/await: вы помечаете слой ViewModel как @MainActor, и компилятор подсвечивает все небезопасные вызовы, которые нужно перенести в фоновые акторы.

Mocking и Actor-контекст

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

Wait для ожидания асинхронных операций

При синхронном тестировании @MainActor-кода используйте XCTestExpectation для ожидания завершения асинхронных операций. Установите ожидание в тесте и выполняйте fulfillment внутри closure, который выполняется на главном потоке. Если тест висит бесконечно — скорее всего, вызов на главном потоке не происходит, и нужно проверить изоляцию актора. Для отладки контекста исполнения полезно добавить проверку Thread.isMainThread внутри тестового кода.

Часто задаваемые вопросы

Обязательно ли помечать весь класс @MainActor?

Нет, достаточно пометить только методы, которые обновляют UI. Однако если в классе несколько таких методов, проще добавить @MainActor ко всему классу. Это гарантирует, что все его члены выполняются на главном потоке, и упрощает поддержку кода.

Чем @MainActor отличается от @globalActor?

@MainActor — это конкретный экземпляр глобального актора, привязанный к главному потоку. @globalActor — это протокол для создания собственных глобальных акторов. Например, вы можете создать @BackgroundActor для выполнения кода на фоновом потоке, если это требуется архитектурой проекта.

Можно ли использовать @MainActor без async/await?

Да, синхронные функции с @MainActor также выполняются на главном потоке. Однако основная ценность @MainActor раскрывается именно с async/await, когда асинхронная функция автоматически возобновляется на главном потоке без ручного переключения через DispatchQueue.main.

Как отменить @MainActor-задачу?

Task.cancel() работает с @MainActor-задачами так же, как с обычными. @MainActor-задача может проверять Task.isCancelled или Throw на CancellationError. При отмене главный поток не блокируется — задача просто прекращает выполнение на ближайшей точке приостановки.

Что будет, если вызвать @MainActor из фонового потока?

Компилятор гарантирует безопасность: если вы вызываете @MainActor-функцию из фонового контекста, компилятор укажет на ошибку. Для асинхронных вызовов достаточно пометить вызывающий код await, и исполнитель сам переключится на главный поток. Для синхронных — требуется явное переключение через MainActor.run.

Итоги

  • @MainActor — глобальный актор Swift, гарантирующий выполнение на главном потоке.
  • Проверка компилятором устраняет целый класс ошибок UI-безопасности.
  • Аннотация всего класса автоматически помещает все его методы на главный поток.
  • MainActor.run — явное переключение для legacy-кода и синхронных контекстов.
  • В отличие от DispatchQueue.main, @MainActor не создаёт замыканий и использует переключение исполнителя.
  • Тяжёлые вычисления не должны выполняться под @MainActor во избежание подвисаний UI.
  • Наследование контекста актора упрощает цепочки асинхронных вызовов и делает код последовательным, предсказуемым и безопасным для UI.

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также