Actor — е тип, представен в Swift 5.5+, който решава проблема с състезание на данни на езиково ниво. За разлика от ръчните заключвания и опашките DispatchQueue, actor автоматично изолира състоянието си и синхронизира достъпа до него. Това означава, че две нишки не могат едновременно да променят едно и също свойство на тип actor, което елиминира състезанието без допълнително усилие от страна на разработчика. Механизмът се базира на концепцията изолация на actor, където компилаторът проследява достъпа до свойствата и методите на actor. Според Apple, 2025, моделът на actor е фундаментална част от система за конкурентност на Swift.
Основни моменти
Actor — е референтен тип, подобен на клас, но с автоматична защита срещу състезание на данни. Беше представен в Swift 5.5 като част от система за конкурентност заедно с async/await и Task. Actor гарантира, че променливото му състояние никога няма да бъде едновременно четено или записвано от различни нишки без изрична синхронизация.
За деклариране на actor се използва ключовата дума actor, следвана от фигурни скоби с неговите членове. Синтактически, actor напомня клас, но поведението му е радикално различно.
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)
// Асинхронен призов — изисква се отвън actor
await account.deposit(amount: 500.0)
let currentBalance = await account.getBalance()
Всеки призов на метод на actor изисква await, защото actor може да е зает с друга задача. Това не е грешка, а съзнателен дизайн за предотвратяване на състезания. Swift имплицитно прави getter и setter на свойствата на actor async, така че четенето на свойство също изисква await.
Actor и класът са референтни типове, но поведението им в многонишкова среда се различава радикално. Класът не осигурява никаква автоматична защита срещу състезания, докато actor го вгражда на ниво компилатор чрез системата от типове.
| Характеристика | Actor | Class |
|---|---|---|
| Защита от състезание | Автоматична, на ниво компилатор | Изисква ръчна синхронизация |
| Достъп до свойства | Само чрез await отвън | Директен, без синхронизация |
| Наследяване | Само от други actor | Стандартно класово |
| Съвместимост с протоколи | Може да съвпада с протоколи | Стандартно |
| Производителност | Нисък оверхейд при изолация | Без синхронизация — по-бързо |
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() {
// Този код гарантирано се изпълнява на главната нишка
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 би могъл да се блокира завинаги, ако един негов метод чака друг, който от своя страна чака първия.
Когато код в актьора изпълнява await, актьорът спира текущата задача и може да обработи друга, поставена в опашката. След завършване на await, задачата се подновява. Това предотвратява deadlock, но изисква внимание: състоянието на actor между точките на await може да се промени.
actor DataProcessor {
var cache: [Int: String] = [:]
func process(id: Int) async -> String {
if let cached = cache[id] {
return cached
}
// await — точка на повторно вхождане
let result = await fetchData(id: id)
// Кешът може да се е променил след await — проверете отново
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 свойство, което връща моментална снимка на състоянието на actor под формата на Sendable структура.
Да, actor може да съвпада с протоколи. Ако протоколът съдържа изолирани изисквания (actor-isolated), те автоматично стават actor-изолирани. За асинхронни методи в протоколите, може да посочите с маркер isolated, че те трябва да се викат на конкретен actor.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също