Что такое Swift Actors и как они помогают с параллелизмом?
Swift Actor — ссылочный тип, защищающий своё состояние от гонок: компилятор запрещает прямой доступ извне без await, а все вызовы сериализуются через внутреннюю очередь. @MainActor — глобальный актор для UI-кода.
Что такое Actor в Swift
Actor — ссылочный тип (объявляется ключевым словом actor), введённый в Swift 5.5 вместе с Swift Concurrency. Компилятор статически гарантирует, что обращение к изменяемому состоянию актора возможно только изнутри самого актора; любой внешний вызов обязан использовать await и автоматически маршрутизируется через серийный executor актора. Это устраняет data race без явных блокировок (NSLock, DispatchSemaphore).
Объявление и использование
actor ImageCache {
private var storage: [URL: UIImage] = [:]
func image(for url: URL) -> UIImage? {
storage[url]
}
func store(_ image: UIImage, for url: URL) {
storage[url] = image
}
}
// Вызов из async-контекста:
let cache = ImageCache()
Task {
await cache.store(image, for: url) // await обязателен снаружи
let cached = await cache.image(for: url)
}
@MainActor — глобальный актор для UI
@MainActor — предопределённый глобальный актор, чей executor всегда работает на главном потоке. Аннотация класса или метода гарантирует, что код выполняется на main thread без явного DispatchQueue.main.async.
@MainActor
class FeedViewModel: ObservableObject {
@Published var items: [FeedItem] = []
func load() async {
// Этот метод всегда выполняется на main thread
let fetched = await FeedService.shared.fetch() // уходит на background
items = fetched // возвращается на main actor
}
}
// Вызов из SwiftUI:
Button("Загрузить") {
Task { await viewModel.load() }
}
nonisolated — выход из изоляции актора
Методы, не трогающие изменяемое состояние, можно пометить nonisolated — они вызываются синхронно без перехода через executor:
actor SessionManager {
private var token: String?
nonisolated var appVersion: String {
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
}
func setToken(_ t: String) { token = t }
}
Sendable и передача данных между акторами
Чтобы значение можно было передавать между акторами, его тип должен соответствовать протоколу Sendable. Struct с let-полями и enum без ассоциированных значений автоматически Sendable. Класс требует явного соответствия и либо final + неизменяемые поля, либо @unchecked Sendable с ручной синхронизацией.
struct FeedItem: Sendable {
let id: UUID
let title: String
let publishedAt: Date
}
actor FeedStore {
func save(_ item: FeedItem) { /* ... */ } // OK: FeedItem — Sendable
}
Actor reentrancy
Актор не блокирует поток — он серийный, но реентерабельный. Между двумя точками await другой вызов может вклиниться и изменить состояние. Это частый источник логических ошибок:
actor Counter {
var value = 0
func incrementIfZero() async {
if value == 0 { // (1) проверяем
await Task.yield() // (2) другой вызов может изменить value
value += 1 // (3) гонка по логике, но не data race!
}
}
}
Подводные камни
- Actor reentrancy: состояние актора может измениться между двумя точками
await— проверяй инвариант после каждого suspension point. - Deadlock через @MainActor: вызов
await mainActorMethod()из синхронного main-thread кода создаёт deadlock; используйTask.detachedили выноси вызов в async-контекст. - Забытый @MainActor на ViewModel: без аннотации
@Published-поля обновляются на background-потоке, что вызывает runtime warning и потенциальный crash в UIKit. - Чрезмерная гранулярность: один глобальный актор на весь слой данных превращается в узкое место; распределяй состояние по независимым акторам.
- @unchecked Sendable без синхронизации: позволяет обойти проверки компилятора, но data race при этом остаётся — использовать только с явным
OSAllocatedUnfairLockили аналогом. - Сильная ссылка в Task:
Task { [weak self] in await self?.update() }— безweak selfзадача удерживает актор дольше, чем нужно. - Отмена задачи: актор не отменяет запущенные Task автоматически при deinit — необходимо хранить
Task-handle и вызыватьtask.cancel()в нужный момент. - Смешивание с GCD: переход из
DispatchQueue.asyncв actor-метод требуетTask { await ... }; прямой вызов actor-метода из completion handler нарушает изоляцию.
Common mistakes
- Сводить «Swift Actors и как они помогают с параллелизмом» к синтаксису и не объяснять модель памяти.
- Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии swift-14.
- Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.
What the interviewer is testing
- Формулирует точную модель для «Swift Actors и как они помогают с параллелизмом» и подтверждает ее корректным примером.
- Умеет связать ответ с система типов, тестированием и отладкой на устройстве.
- Называет ограничения подхода swift-14, включая производительность, память и сопровождение.