Swift Concurrency runs code in two places: the main thread and the Cooperative Thread Pool. Two rules decide which one a function gets, and knowing them stops you from blocking the UI or mutating UI state off the main thread.
The Two Rules
- A function isolated to an actor runs on that actor, async or not. Actors run on the
Cooperative Thread Pool, except
@MainActor, which runs on the main thread. - A function with no actor isolation runs on the Cooperative Thread Pool when it is
async, and on the calling thread when it is not.
The pool caps its thread count at the CPU core count, which prevents thread explosion. Keep heavy work there and keep UI updates on the main actor.
Actor Isolation Wins Over Async
Both functions below run on the main thread, always, because the whole type is @MainActor
isolated. The async keyword changes nothing about where they run.
@MainActor final class Store {
var messages: [String] = []
func boo() {
messages = ["boo"]
}
func foo() async {
messages = ["foo"]
}
}
SwiftUI Views Are MainActor Isolated
A view's methods inherit @MainActor isolation from the View protocol. So both calls below run
on the main thread, including the awaited async one, which surprises many people.
struct ContentView: View {
var body: some View {
Text("Hello")
.task { boo() }
.task { await foo() }
}
func boo() { /* main thread */ }
func foo() async { /* still the main thread */ }
}
Escaping With nonisolated
Mark the function nonisolated to drop the inherited isolation. A nonisolated async function
falls under rule 2 and runs on the Cooperative Thread Pool, which is where heavy work belongs.
nonisolated func foo() async {
// runs on the Cooperative Thread Pool
}