Skip to content
Actors and Structured Concurrency
step 1/7

Reading — step 1 of 7

Learn

~3 min readGenerics, Concurrency, and Memory

Swift 5.5 (2021) introduced structured concurrency — async/await, tasks, and actors. The most important concurrency model change in Swift's history.

Note: Judge0's Swift is older — these features may not run there. The lesson explains the semantics and syntax.

async/await

Mark async functions:

func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

Call with await:

func loadProfile() async throws {
    let user = try await fetchUser(id: 42)
    print(user.name)
}

.await yields the current task while waiting — same idea as JavaScript or Rust.

Tasks — entering async from sync

Task {
    let user = try await fetchUser(id: 42)
    print(user)
}

Task { ... } creates a top-level task that runs concurrently. Returns a handle you can cancel:

let task = Task { try await longComputation() }
task.cancel()           // cooperative — runtime checks for cancellation

async let — concurrent child tasks

func loadAll() async throws -> Profile {
    async let user = fetchUser(id: 1)
    async let posts = fetchPosts(userID: 1)
    
    return try await Profile(
        user: user,
        posts: posts
    )
}

Both fetches run concurrently. The await at the end joins them. Less ceremony than withTaskGroup for known fan-out.

Task groups

For dynamic fan-out:

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

All tasks in the group are children of the parent — cancellation cascades, lifetime is bounded.

Actors — safe shared state

A class for concurrent code with automatic mutual exclusion:

actor Counter {
    private var count = 0
    
    func increment() {
        count += 1
    }
    
    func get() -> Int {
        return count
    }
}

let c = Counter()
await c.increment()           // method call requires await
let v = await c.get()

Inside the actor, code is sequential — only one task at a time. Outside, callers await because the actor's task may need to wait for an opening.

No Mutex, no atomic ops, no synchronized — the type system enforces the safety.

@MainActor

For UI code (or any single-thread requirement):

@MainActor
class ViewModel: ObservableObject {
    @Published var users: [User] = []
    
    func load() async {
        let fetched = try? await fetchAll(ids: [1, 2, 3])
        users = fetched ?? []          // safe — running on main thread
    }
}

Methods on a @MainActor type run on the main thread. Cross-actor calls await — even from background tasks.

Sendable

For a type to be safely passed between concurrency contexts, it must be Sendable:

  • Value types (struct, enum) made of Sendable parts are Sendable
  • Final classes with only let immutable Sendable storage are Sendable
  • Actors are Sendable by definition
  • Reference types with shared mutable state are NOT Sendable

The compiler tracks this and errors if you try to capture a non-Sendable in an async closure crossing concurrency boundaries.

Common mistakes

  • Calling actor methods without await — compile error. Reminder that the call may suspend.
  • Using Task.detached for everything — detached tasks lose structured concurrency benefits (cancellation propagation, deadline inheritance). Prefer Task { ... }.
  • Mutating shared state from multiple tasks without an actor — data race. The compiler increasingly catches this.
  • Holding a non-Sendable value across an await — error. Either make the type Sendable or restructure.
  • Forgetting that actor methods are reentrant — between an await inside an actor method, ANOTHER caller may take a turn. Don't assume invariants hold across awaits.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…