Lesson 20 / 25

Actors, MainActor, Sendable and Swift 6 Data-Race Safety

Protect shared mutable state with actors and understand Swift 6 checking.

Data races caught by the compiler

A data race occurs when two threads access the same memory at the same time and at least one writes. Swift's answer is isolation. An actor is a reference type whose mutable state can only be accessed by one task at a time; calls from outside the actor are asynchronous (await counter.increment()), and the actor serialises them, so you get thread safety without manual locks. @MainActor is a global actor representing the main thread: UI code and view models are annotated with it so UI updates always happen on the main thread. Sendable marks types that are safe to pass between concurrency domains: value types with sendable contents, actors, and immutable or internally synchronised classes. In the Swift 6 language mode, the compiler checks isolation and sendability at compile time and reports potential data races as errors. Migrating existing code is usually gradual: enable strict concurrency warnings first, annotate types, and fix issues module by module. Later Swift releases add options that make this easier, such as running code on the main actor by default in app targets.

An actor and a MainActor view model

The actor serialises access; the view model updates UI state on the main actor.

actor InventoryStore {
    private var stock: [String: Int] = ["PEN-BLUE": 10]

    func reserve(_ sku: String, quantity: Int) -> Bool {
        let available = stock[sku, default: 0]
        guard available >= quantity else { return false }
        stock[sku] = available - quantity               // no other task can interleave here
        return true
    }

    func available(_ sku: String) -> Int { stock[sku, default: 0] }
}

@MainActor
@Observable
final class CheckoutViewModel {
    var message = ""
    private let store: InventoryStore

    init(store: InventoryStore) { self.store = store }

    func buy(_ sku: String, quantity: Int) async {
        let ok = await store.reserve(sku, quantity: quantity)   // hop to the actor and back
        message = ok ? "Reserved \(quantity)" : "Out of stock"   // safe: we are on the main actor
    }
}

struct OrderLine: Sendable {      // value type with sendable properties: safe to share
    let sku: String
    let quantity: Int
}

A bank teller window

An actor is a bank teller who serves one customer at a time through a window. Customers queue (await) instead of all reaching into the cash drawer at once, so the balance is never corrupted.

Quick check: What does the Swift 6 language mode add to concurrency?

  • A garbage collector
  • Automatic multithreading of all loops
  • Removal of async/await
  • Compile-time checking of isolation and Sendable to prevent data races
Answer

Compile-time checking of isolation and Sendable to prevent data races — Swift 6 turns potential data races into compile-time errors.