Lesson 95Advanced Topics

Actors & Data Safety

What are Actors?

Actors are reference types like classes, but they protect their mutable state from data races. Only one task can access an actor's state at a time.

Actor vs Class

Class (Unsafe)

Concurrent access causes data races

Must manually synchronize

Actor (Safe)

Automatic isolation

Compiler-enforced safety

Key Concepts

actor

Isolated reference type

Access with await

nonisolated

Skip isolation for safe members

No await needed

@MainActor

Run on main thread

For UI updates

Sendable

Safe to cross boundaries

Value types are Sendable

Actor Isolation

actor Counter {
    var count = 0
    func increment() { count += 1 }
}

let c = Counter()
await c.increment() // Must await

MainActor for UI

Use @MainActor for any code that updates UI. This ensures it runs on the main thread, preventing UI glitches and crashes.

@MainActor class ViewModel { ... }
main.swift
// ACTORS & DATA SAFETY
// Protect shared mutable state in concurrent code

// ===== THE PROBLEM: DATA RACES =====
// Without protection, concurrent access causes bugs
class UnsafeCounter {
    var count = 0

    func increment() {
        count += 1  // Not thread-safe!
    }
}

// ===== ACTORS: THE SOLUTION =====
actor SafeCounter {
    var count = 0

    func increment() {
        count += 1  // Protected by actor
    }

    func getCount() -> Int {
        return count
    }
}

let counter = SafeCounter()

// Must use await to access actor methods/properties
Task {
    await counter.increment()
    let value = await counter.getCount()
    print("Count: \(value)")
}

// ===== ACTOR ISOLATION =====
actor BankAccount {
    let owner: String
    private var balance: Double

    init(owner: String, balance: Double) {
        self.owner = owner
        self.balance = balance
    }

    // Synchronous access inside actor
    func deposit(_ amount: Double) {
        balance += amount
    }

    func withdraw(_ amount: Double) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }

    func getBalance() -> Double {
        return balance
    }
}

let account = BankAccount(owner: "Alice", balance: 1000)

Task {
    await account.deposit(500)
    let success = await account.withdraw(200)
    print("Withdrawal success: \(success)")
    print("Balance: \(await account.getBalance())")
}

// ===== NONISOLATED =====
// Properties/methods that don't need isolation
actor User {
    let id: String           // let is implicitly nonisolated
    var name: String

    init(id: String, name: String) {
        self.id = id
        self.name = name
    }

    // Doesn't access mutable state
    nonisolated func getID() -> String {
        return id  // Can be called without await
    }

    func getName() -> String {
        return name  // Needs await
    }
}

let user = User(id: "123", name: "Bob")
print(user.id)           // No await needed
print(user.getID())      // No await needed

Task {
    print(await user.getName())  // Needs await
}

// ===== MAINACTOR =====
// Ensures code runs on main thread (for UI)
@MainActor
class ViewModel {
    var data: [String] = []

    func updateUI() {
        // Safe to update UI here
        data.append("New item")
    }
}

// Mark individual functions
actor DataManager {
    var items: [String] = []

    @MainActor
    func updateUI(with newItems: [String]) {
        // Runs on main thread
        print("Updating UI with \(newItems.count) items")
    }

    func fetchData() async {
        // Simulate fetch
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        items = ["A", "B", "C"]

        // Switch to main thread for UI
        await updateUI(with: items)
    }
}

// ===== SENDABLE =====
// Types that are safe to share between actors

// Value types are Sendable by default
struct Point: Sendable {
    var x: Int
    var y: Int
}

// Classes need @unchecked if you ensure thread safety
final class ThreadSafeCache: @unchecked Sendable {
    private let lock = NSLock()
    private var cache: [String: Any] = [:]

    func set(_ value: Any, for key: String) {
        lock.lock()
        cache[key] = value
        lock.unlock()
    }
}

// ===== ASYNC SEQUENCES WITH ACTORS =====
actor MessageQueue {
    private var messages: [String] = []

    func send(_ message: String) {
        messages.append(message)
    }

    func receive() -> String? {
        return messages.isEmpty ? nil : messages.removeFirst()
    }

    func getAllMessages() -> [String] {
        return messages
    }
}

// ===== GLOBAL ACTORS =====
@globalActor
actor DatabaseActor {
    static let shared = DatabaseActor()
}

@DatabaseActor
func saveToDatabase(_ data: String) {
    print("Saving: \(data)")
}

// ===== ACTOR REENTRANCY =====
actor ImageLoader {
    var cache: [String: Data] = [:]

    func loadImage(url: String) async -> Data {
        // Check cache first
        if let cached = cache[url] {
            return cached
        }

        // Suspension point - other calls can run here!
        let data = await downloadImage(url: url)

        // Check again after suspension
        if let cached = cache[url] {
            return cached
        }

        cache[url] = data
        return data
    }

    func downloadImage(url: String) async -> Data {
        try? await Task.sleep(nanoseconds: 500_000_000)
        return Data()
    }
}

Try It Yourself!

Create a ShoppingCart actor with thread-safe add, remove, and total calculation methods!