Skip to content
advanced Phase 14 · Advanced iOS

Advanced Concurrency

Master actors, actor isolation, Sendable, structured concurrency, and concurrency debugging.

1h
4 problems
Topic Progress 0%

Actor Isolation

What are Actors?

Actors are reference types that protect their mutable state by ensuring only one task can access them at a time. This eliminates data races at compile time:

actor BankAccount {
    var balance: Decimal
    let accountNumber: String
    
    init(balance: Decimal, accountNumber: String) {
        self.balance = balance
        self.accountNumber = accountNumber
    }
    
    func deposit(_ amount: Decimal) {
        balance += amount
    }
    
    func withdraw(_ amount: Decimal) throws {
        guard balance >= amount else { throw BankError.insufficientFunds }
        balance -= amount
    }
}

Accessing Actor State

All property access and method calls go through the actor isolation boundary:

let account = BankAccount(balance: 1000, accountNumber: "123")

// Must use await for any access
await account.deposit(500)
let balance = await account.balance

// Sequential access within a task
await account.deposit(100)
await account.withdraw(50) // Guaranteed no data race

nonisolated

Mark methods as nonisolated when they do not access actor state:

actor DataStore {
    private var cache: [String: Any] = [:]
    
    // nonisolated: does not access cache
    nonisolated func description() -> String {
        "DataStore instance"
    }
    
    // isolated: accesses cache
    func store(_ value: Any, forKey key: String) {
        cache[key] = value
    }
}

Actor Reentrancy

Actors are reentrant, meaning they can yield control during awaits. This requires careful state management:

actor FileManager {
    func processFile(named name: String) async throws -> Data {
        let data = try await download(name) // May yield here
        // State may have changed by the time we resume
        let processed = transform(data) // Must revalidate state
        return processed
    }
}

Never assume actor state is unchanged across await points.

Sendable Protocol

What is Sendable?

Sendable marks types that can be safely passed across concurrency domains. It ensures the type has no mutable shared state:

struct UserInfo: Sendable {
    let name: String
    let email: String
    let preferences: UserPreferences
}

struct UserPreferences: Sendable {
    let theme: String
    let notifications: Bool
}

Sendable Requirements

A type is Sendable if:

  • It is a value type (struct, enum) with Sendable properties
  • It is a final class with no mutable state
  • It is an actor (actors are implicitly Sendable)
  • It is marked as Sendable and the compiler verifies safety

@Sendable Closures

Closures that cross isolation boundaries must be marked @Sendable:

func processInBackground(_ data: Data, completion: @Sendable @escaping (Result<ProcessedData, Error>) -> Void) {
    Task.detached {
        let result = await process(data)
        completion(.success(result))
    }
}

Non-Sendable Types

Classes with mutable state are not Sendable. Use actors instead:

// NOT Sendable - has mutable state
class NetworkCache {
    var cache: [String: Data] = [:] // Mutable!
}

// Sendable - actor protects mutable state
actor NetworkCache {
    var cache: [String: Data] = [:]
}

Sendable and Swift Evolution

Sendable is part of Swift push toward data-race safety. In future Swift versions, Sendable conformance will be required more strictly. Adopt it now to prepare.

Structured Concurrency

TaskGroup

TaskGroup manages a collection of child tasks:

func fetchAllUsers(ids: [UUID]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask { try await self.fetchUser(id: id) }
        }
        var users: [User] = []
        for try await user in group {
            users.append(user)
        }
        return users
    }
}

TaskGroup Benefits

  • Automatic cancellation propagation
  • Structured lifetime management
  • Error handling for all child tasks
  • Memory safety with automatic cleanup

withThrowingTaskGroup vs withTaskGroup

  • withThrowingTaskGroup: child tasks can throw, group throws if any child throws
  • withTaskGroup: child tasks cannot throw
// Non-throwing variant
await withTaskGroup(of: Void.self) { group in
    for item in items {
        group.addTask { await self.process(item) }
    }
}

Unstructured Tasks

When you need more control over task lifecycle:

// Detached task: no parent-child relationship
let task = Task.detached {
    await longRunningOperation()
}

// Cancel when needed
task.cancel()

// Check cancellation
if Task.isCancelled { return }

Task Priority

Task.detached(priority: .high) {
    await criticalOperation()
}

// Current task priority
if let priority = Task.currentPriority {
    print("Current priority: \(priority)")
}

Best Practices

  • Use structured concurrency (TaskGroup) when possible
  • Avoid unstructured tasks unless you need explicit lifecycle control
  • Always handle errors in throwing task groups
  • Propagate cancellation for responsive cancellation
  • Set appropriate task priorities for user-facing work

Quiz

1. What do actors protect against?

Question 1 options

2. What does the Sendable protocol guarantee?

Question 2 options

3. What happens in a TaskGroup when a child task throws?

Question 3 options

4. Why must you use await when accessing actor properties?

Question 4 options

Flashcards

Question

What is actor isolation?

Answer

The property that ensures only one task can access an actor mutable state at a time, preventing data races at compile time.

Question

What is the difference between withTaskGroup and withThrowingTaskGroup?

Answer

withTaskGroup handles non-throwing tasks. withThrowingTaskGroup handles throwing tasks and propagates errors from any child task.

Question

Why must closures crossing isolation boundaries be @Sendable?

Answer

To ensure the closure does not capture mutable state unsafely when moving between different concurrency domains.

Revision Notes

Key Takeaways

  • 1. Actors prevent data races through compile-time isolation
  • 2. Sendable ensures types are safe to pass across concurrency domains
  • 3. TaskGroup provides structured concurrency with automatic cancellation
  • 4. nonisolated optimizes actor methods that do not access state
  • 5. Always revalidate actor state after await points due to reentrancy

Interview Tips

  • Explain how actors prevent data races
  • Describe the difference between actors and classes
  • Discuss when to use structured vs unstructured concurrency
  • Walk through handling errors in TaskGroup

Cheat Sheet

Advanced Concurrency Quick Reference

  • Actor: protects mutable state from data races
  • Sendable: safe to cross concurrency domains
  • nonisolated: skip actor isolation for pure functions
  • TaskGroup: structured child task management
  • @Sendable: required for cross-isolation closures
  • Actor reentrancy: always revalidate state after await