Reading — step 1 of 7
Learn
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.detachedfor everything — detached tasks lose structured concurrency benefits (cancellation propagation, deadline inheritance). PreferTask { ... }. - 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
awaitinside 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…