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 неявно робить геттери та сеттери властивостей 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, який позначає класи, властивості або методи як виконувані на головному потоці. Це особливо корисно для UI-коду в iOS-додатках.
@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 міг би заблокуватися назавжди, якби один його метод очікував інший, який своєю чергою очікує перший.
Коли код всередині 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 — точка повторного входу
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-ізольованими. Для асинхронних методів у протоколах можна вказати, що вони повинні викликатися на конкретному actor за допомогою маркера isolated.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.