Actor — это тип, представленный в Swift 5.5+, который решает проблему гонок данных на уровне языка. В отличие от ручных блокировок и очередей DispatchQueue, actor автоматически изолирует своё состояние и синхронизирует доступ к нему. Это значит, что два потока не могут одновременно изменить одно и то же свойство actor-типа, что исключает состояние гонки без дополнительных усилий со стороны разработчика. Механизм основан на концепции actor-изоляции, когда компилятор отслеживает доступ к свойствам и методам actor. По данным Apple, 2025, actor-модель является фундаментальной частью системы конкурентности Swift.
Главное
Actor — это ссылочный тип, аналогичный классу, но с автоматической защитой от гонок данных. Он был представлен в Swift 5.5 как часть системы конкурентности наряду с async/await и Task. Actor гарантирует, что его изменяемое состояние никогда не будет одновременно прочитано или записано из разных потоков без явной синхронизации.
Для объявления actor используется ключевое слово actor, после которого следуют фигурные скобки с его членами. Синтаксически actor напоминает класс, но поведение radically отличается.
actor BankAccount {
private var balance: Double
init(initialBalance: Double) {
self.balance = initialBalance
}
func deposit(amount: Double) {
balance += amount
}
func getBalance() -> Double {
return balance
}
}
Компилятор автоматически изолирует все свойства и методы actor так, что они становятся доступны только внутри actor-контекста. При попытке обратиться к balance извне actor компилятор выдаст ошибку, если вызов не помечен как async.
Изоляция данных — это ключевая концепция actor. Actor гарантирует взаимно исключающий доступ к своему состоянию через механизм actor-исполнителя (actor executor). Каждый actor имеет собственный исполнитель, который обрабатывает все обращения к его изолированным членам последовательно.
Когда код вне actor вызывает его метод, вызов помещается в очередь actor-исполнителя. Actor выполняет только одну задачу одновременно, что гарантирует отсутствие гонок. Если два потока одновременно вызывают deposit, второй вызов ожидает завершения первого.
let account = BankAccount(initialBalance: 1000.0)
// Async call — required from outside actor
await account.deposit(amount: 500.0)
let currentBalance = await account.getBalance()
Каждый вызов метода actor требует await, потому что actor может быть занят другой задачей. Это — не баг, а сознательный дизайн, предотвращающий гонки. Swift неявно делает геттеры и сеттеры свойств actor async, поэтому чтение свойства тоже требует await.
Actor и класс являются ссылочными типами, но их поведение в многопоточной среде кардинально различается. Класс не обеспечивает никакой автоматической защиты от гонок, тогда как actor встраивает её на уровне компилятора через систему типов.
| Характеристика | Actor | Class |
|---|---|---|
| Защита от гонок | Автоматическая, на уровне компилятора | Требует ручной синхронизации |
| Доступ к свойствам | Только через await извне | Прямой, без синхронизации |
| Наследование | Только от других actor | Стандартное классовое |
| Protocol conformance | Может conformиться протоколам | Стандартное |
| Performance | Низкий overhead при изоляции | Без синхронизации — быстрее |
Actor может наследоваться только от другого actor и не может наследоваться от класса. Это сделано сознательно, потому что класс не имеет механизма actor-изоляции, и смешивание двух моделей привело бы к неопределённому поведению.
actor SavingsAccount: BankAccount {
func applyInterest(rate: Double) {
let interest = balance * rate
balance += interest
}
}
Асинхронные вызовы — это механизм взаимодействия с actor из внешнего кода. Поскольку actor изолирует своё состояние, любой доступ к его членам извне требует await. Это позволяет Swift гарантировать, что вызывающий код не блокирует поток и actor может обрабатывать другие запросы.
Помимо объявленных actor-типов, Swift поддерживает глобальные actor — атрибут @MainActor, который помечает классы, свойства или методы как исполняемые на главном потоке. Это особенно полезно при работе с UIKit и SwiftUI.
@MainActor
class ViewModel: ObservableObject {
@Published var title: String = ""
func updateTitle() {
// This code is guaranteed to run on the main thread
title = "New Title"
}
}
Использование @MainActor избавляет от необходимости вручную вызывать DispatchQueue.main.async, что делает код чище и безопаснее. Компилятор проверяет, что переключение на главный поток происходит корректно.
Nonisolated — это ключевое слово, позволяющее пометить метод или вычисляемое свойство actor как не изолированное. Такие члены не имеют доступа к actor-изолированному состоянию, но могут вызываться без await извне actor.
Nonisolated методы полезны для вычислений, не зависящих от изменяемого состояния actor. Например, метод formatBalance не обращается к balance напрямую, а только форматирует переданное значение — такой метод безопасно сделать nonisolated.
actor BankAccount {
private var balance: Double = 0
nonisolated func formatBalance(amount: Double) -> String {
return "$\(amount)"
}
}
nonisolated-члены выполняются синхронно и не требуют await. Однако они не могут читать actor-изолированные свойства напрямую. Если nonisolated методу нужно значение из actor, его нужно передать параметром.
Reentrancy — это механизм, разрешающий повторный вход в actor во время ожидания асинхронного вызова. Без reentrancy actor мог бы заблокироваться навсегда, если бы один его метод ожидал другой, который в свою очередь ожидает первый.
Когда код внутри actor выполняет await, actor приостанавливает текущую задачу и может обработать другую, поставленную в очередь. После завершения await задача возобновляется. Это предотвращает deadlock, но требует осторожности: состояние actor между точками await может измениться.
actor DataProcessor {
var cache: [Int: String] = [:]
func process(id: Int) async -> String {
if let cached = cache[id] {
return cached
}
// await — reentrancy point
let result = await fetchData(id: id)
// Cache may have changed after await — recheck
cache[id] = result
return result
}
}
Разработчики должны учитывать reentrancy и проверять состояние actor после точек await. Типичная ошибка — предположение, что actor-изоляция сохраняется сквозь асинхронные паузы. На практике между await и следующим оператором состояние может отличаться от ожидаемого.
Часто задаваемые вопросы
Actor автоматически изолирует своё состояние от гонок данных, требуя await для доступа извне. Класс не предоставляет такой защиты — разработчик сам отвечает за синхронизацию через блокировки или очереди. Actor наследуется только от actor, класс — от класса.
Да, actor может наследоваться от другого actor. При этом подкласс получает все изолированные свойства и методы родителя. Actor не может наследоваться от класса, поскольку классы не имеют механизма actor-изоляции на уровне компилятора.
actor-isolated контекст — это область кода, в которой разрешён прямой доступ к изменяемому состоянию actor. Внутри методов actor, помеченных как isolated (по умолчанию), можно читать и писать свойства без await. Компилятор проверяет границы изоляции.
Данные из actor передаются через async-методы, которые возвращают Sendable-типы, или через nonisolated методы, принимающие значения параметрами. Можно также создать async-свойство, которое возвращает Snapshot состояния actor в виде Sendable-структуры.
Да, actor может соответствовать протоколам. Если протокол содержит изолированные требования (actor-isolated), они автоматически становятся actor-изолированными. Для асинхронных методов в протоколах можно указать, что они должны вызываться на конкретном actor с помощью маркера isolated.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также