How do you define your own global actor, what does annotating a type with it do, and when is it better than an actor instance or the main actor?
Predict first
Ten tasks each add 100 items to a @Store class. What is the count?
@globalActor actor Store { static let shared = Store() }
@Store final class Cache {
var items: [Int] = []
func add(_ n: Int) { items.append(n) }
}
@Store func fill(_ c: Cache) { for i in 0..<100 { c.add(i) } }
let c = await Cache()
await withTaskGroup(of: Void.self) { g in
for _ in 0..<10 { g.addTask { await fill(c) } }
}
print(await c.items.count)
- 1000
- Fewer than 1000, a data race
- Compile error
Mental model. A global actor is a single shared reception desk for a whole department. Anyone who wants department records queues at the same desk, regardless of which team they are on.
A global actor is an actor with a static let shared instance, marked @globalActor. Annotating a function, property, or whole type with @Store makes all its members isolated to that one shared executor, across files and types. The check marks Cache and fill with @Store: ten concurrent callers each append 100 items and the result is exactly 1000, because every access is serialised on the same actor even though Cache is a plain class with no locks.
The value is sharing one isolation domain across many types without passing an actor instance around: a database layer (@DatabaseActor), an audio engine, a legacy subsystem you are fencing in. @MainActor is simply the built-in global actor tied to the main thread. Prefer an ordinary actor instance when the state belongs to an object with its own lifetime (an account, a download); prefer a global actor when the state is conceptually one subsystem. Cost and caution: every cross-boundary call is an await; a global actor can become a bottleneck if unrelated work all queues on it; and unlike the main actor it gives you no thread guarantee, only serial access.
Everything isolated to @Store runs serially on its shared executor, so no update is lost.
Common traps
- Putting unrelated work on one global actor. Everything queues behind everything else.
- Assuming a custom global actor runs on a particular thread. Only
@MainActorpromises the main thread.
They might follow up with
- Can a type be partly isolated? Yes: annotate the type, then mark specific members
nonisolated, or annotate only some members. - How does isolation propagate to subclasses and closures? Subclasses inherit it, and closures formed in isolated code inherit it unless
@Sendable(Q296).
Related questions
- 10.9.2 What is an isolated parameter, and when would you write a function that takes one instead of making it an…
- 10.9.4 Where does code actually run after an await: what do @MainActor, MainActor.run and a nonisolated async…
- 10.10.2 How do you choose between an actor, a Mutex, an unfair lock, an atomic and a serial queue to protect shared…
- 10.4.2 How do @MainActor, global actors, isolated and nonisolated work, and how does isolation propagate?
- 10.5.2 How do you migrate an existing app to Swift 6 and strict concurrency without breaking it?
← What is an isolated parameter, and when would you write a function… Where does code actually run after an await: what do @MainActor… →