Actor — to typ wprowadzony w Swift 5.5+, który rozwiązuje problem wyścigów danych na poziomie języka. W przeciwieństwie do ręcznych blokad i kolejek DispatchQueue, actor automatycznie izoluje swój stan i synchronizuje dostęp do niego. Oznacza to, że dwa wątki nie mogą jednocześnie zmienić tej samej właściwości typu actor, co eliminuje stan wyścigu bez dodatkowego wysiłku ze strony programisty. Mechanizm opiera się na koncepcji izolacji aktora, gdzie kompilator śledzi dostęp do właściwości i metod aktora. Według Apple, 2025, model aktora jest fundamentalną częścią systemu współbieżności Swift.
Najważniejsze
Actor — to typ referencyjny, podobny do klasy, ale z automatyczną ochroną przed wyścigami danych. Został wprowadzony w Swift 5.5 jako część systemu współbieżności wraz z async/await i Task. Actor gwarantuje, że jego zmienny stan nigdy nie zostanie jednocześnie odczytany lub zapisany z różnych wątków bez jawnej synchronizacji.
Do deklaracji aktora używa się słowa kluczowego actor, po którym następują nawiasy klamrowe z jego członkami. Składniowo actor przypomina klasę, ale zachowanie radykalnie się różni.
actor BankAccount {
private var balance: Double
init(initialBalance: Double) {
self.balance = initialBalance
}
func deposit(amount: Double) {
balance += amount
}
func getBalance() -> Double {
return balance
}
}
Kompilator automatycznie izoluje wszystkie właściwości i metody aktora, tak że stają się dostępne tylko wewnątrz kontekstu aktora. Próba dostępu do balance spoza aktora spowoduje błąd kompilacji, jeśli wywołanie nie jest oznaczone jako async.
Izolacja danych — to kluczowa koncepcja aktora. Actor gwarantuje wzajemnie wykluczający dostęp do swojego stanu poprzez mechanizm wykonawcy aktora (actor executor). Każdy actor ma własnego wykonawcę, który przetwarza wszystkie dostępy do jego izolowanych członków sekwencyjnie.
Gdy kod spoza aktora wywołuje jego metodę, wywołanie jest umieszczane w kolejce wykonawcy aktora. Actor wykonuje tylko jedno zadanie jednocześnie, co gwarantuje brak wyścigów. Jeśli dwa wątki jednocześnie wywołają deposit, drugie wywołanie czeka na zakończenie pierwszego.
let account = BankAccount(initialBalance: 1000.0)
// Wywołanie asynchroniczne — wymagane spoza aktora
await account.deposit(amount: 500.0)
let currentBalance = await account.getBalance()
Każde wywołanie metody aktora wymaga await, ponieważ actor może być zajęty innym zadaniem. To nie jest błąd, lecz świadomy projekt zapobiegający wyścigom. Swift niejawnie czyni gettery i settery właściwości aktora asynchronicznymi, dlatego odczyt właściwości również wymaga await.
Actor i klasa są typami referencyjnymi, ale ich zachowanie w środowisku wielowątkowym radykalnie się różni. Klasa nie zapewnia żadnej automatycznej ochrony przed wyścigami, podczas gdy actor wbudowuje ją na poziomie kompilatora poprzez system typów.
| Cecha | Actor | Class |
|---|---|---|
| Ochrona przed wyścigami | Automatyczna, na poziomie kompilatora | Wymaga ręcznej synchronizacji |
| Dostęp do właściwości | Tylko przez await z zewnątrz | Bezpośredni, bez synchronizacji |
| Dziedziczenie | Tylko od innych aktorów | Standardowe klasowe |
| Zgodność z protokołami | Może zgodnie z protokołami | Standardowe |
| Wydajność | Niski narzut przy izolacji | Bez synchronizacji — szybsze |
Actor może dziedziczyć tylko po innym actorze i nie może dziedziczyć po klasie. Jest to świadome działanie, ponieważ klasa nie ma mechanizmu izolacji aktora, a mieszanie dwóch modeli prowadziłoby do nieokreślonego zachowania.
actor SavingsAccount: BankAccount {
func applyInterest(rate: Double) {
let interest = balance * rate
balance += interest
}
}
Wywołania asynchroniczne — to mechanizm interakcji z aktorem z zewnętrznego kodu. Ponieważ actor izoluje swój stan, każdy dostęp do jego członków z zewnątrz wymaga await. Pozwala to Swift zagwarantować, że wywołujący kod nie blokuje wątku, a actor może przetwarzać inne żądania.
Oprócz zadeklarowanych typów aktora, Swift obsługuje globalne aktory — atrybut @MainActor, który oznacza klasy, właściwości lub metody jako wykonywane na głównym wątku. Jest to szczególnie przydatne podczas pracy z UIKit i SwiftUI.
@MainActor
class ViewModel: ObservableObject {
@Published var title: String = ""
func updateTitle() {
// Ten kod jest gwarantowanie wykonywany na głównym wątku
title = "New Title"
}
}
Używanie @MainActor eliminuje konieczność ręcznego wywoływania DispatchQueue.main.async, co czyni kod czystszym i bezpieczniejszym. Kompilator sprawdza, czy przełączenie na główny wątek następuje poprawnie.
Nonisolated — to słowo kluczowe umożliwiające oznaczenie metody lub właściwości obliczanej aktora jako nieizolowanej. Takie członki nie mają dostępu do stanu izolowanego aktora, ale mogą być wywoływane bez await z zewnątrz aktora.
Metody nonisolated są przydatne do obliczeń niezależnych od zmiennego stanu aktora. Na przykład metoda formatBalance nie ma bezpośredniego dostępu do balance, a jedynie formatuje przekazaną wartość — taką metodę można bezpiecznie uczynić nonisolated.
actor BankAccount {
private var balance: Double = 0
nonisolated func formatBalance(amount: Double) -> String {
return "$\(amount)"
}
}
Członki nonisolated są wykonywane synchronicznie i nie wymagają await. Nie mogą jednak bezpośrednio odczytywać izolowanych właściwości aktora. Jeśli metoda nonisolated potrzebuje wartości z aktora, należy ją przekazać jako parametr.
Reentrancy — to mechanizm umożliwiający ponowne wejście do aktora podczas oczekiwania na wywołanie asynchroniczne. Bez reentrancy actor mógłby zablokować się na zawsze, gdyby jedna jego metoda czekała na drugą, która z kolei czeka na pierwszą.
Gdy kod wewnątrz aktora wykonuje await, actor zawiesza bieżące zadanie i może przetworzyć inne umieszczone w kolejce. Po zakończeniu await zadanie jest wznawiane. Zapobiega to deadlockom, ale wymaga ostrożności: stan aktora między punktami await może się zmienić.
actor DataProcessor {
var cache: [Int: String] = [:]
func process(id: Int) async -> String {
if let cached = cache[id] {
return cached
}
// await — punkt ponownego wejścia
let result = await fetchData(id: id)
// Pamięć podręczna mogła się zmienić po await — sprawdź ponownie
cache[id] = result
return result
}
}
Programiści muszą uwzględniać reentrancy i sprawdzać stan aktora po punktach await. Typowym błędem jest założenie, że izolacja aktora utrzymuje się przez asynchroniczne pauzy. W praktyce między await a następną instrukcją stan może odbiegać od oczekiwanego.
Często zadawane pytania
Actor automatycznie izoluje swój stan od wyścigów danych, wymagając await do dostępu z zewnątrz. Klasa nie zapewnia takiej ochrony — programista sam odpowiada za synchronizację przez blokady lub kolejki. Actor dziedziczy tylko po actorze, klasa — po klasie.
Tak, actor może dziedziczyć po innym actorze. Podklasa otrzymuje wszystkie izolowane właściwości i metody rodzica. Actor nie może dziedziczyć po klasie, ponieważ klasy nie mają mechanizmu izolacji aktora na poziomie kompilatora.
Kontekst actor-isolated — to obszar kodu, w którym dozwolony jest bezpośredni dostęp do zmiennego stanu aktora. Wewnątrz metod aktora oznaczonych jako isolated (domyślnie) można czytać i pisać właściwości bez await. Kompilator sprawdza granice izolacji.
Dane z aktora są przekazywane przez metody async, które zwracają typy Sendable, lub przez metody nonisolated przyjmujące wartości jako parametry. Można również utworzyć właściwość async, która zwraca migawkę stanu aktora w postaci struktury Sendable.
Tak, actor może zgodnie z protokołami. Jeśli protokół zawiera wymagania izolowane (actor-isolated), stają się one automatycznie izolowane przez aktora. W przypadku metod asynchronicznych w protokołach można określić, że mają być wywoływane na konkretnym actorze za pomocą znacznika isolated.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również