Step 1 of 6 · Reading · ~4 min
Learn
Concurrency
Swift 5.5 added native async/await and structured concurrency. The model is similar to JavaScript and C#, but Swift's takes type-safety and cancellation seriously.
Basic async/await
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)
}
// Calling — must be in an async context:
let user = try await fetchUser(id: 42)
async marks a function as suspending — it can pause without blocking a thread. await is required at every call site to make suspension visible.
Tasks — top-level async work
Most code starts an async chain via a Task:
let task = Task {
let user = try await fetchUser(id: 42)
print(user.name)
}
// Cancel from outside:
task.cancel()
Parallel execution with async let
func fetchProfile(id: Int) async throws -> (User, [Post]) {
async let user = fetchUser(id: id)
async let posts = fetchPosts(userId: id)
return try await (user, posts) // both run in parallel
}
async let kicks off the work immediately. The await later collects the result. Total time = max(individual times), not sum.
TaskGroup — N parallel tasks
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
}
}
Dynamic number of parallel tasks. Cancellation propagates: if you throw, all sibling tasks are cancelled.
Cancellation
Cancellation in Swift is cooperative — your code must check:
func longComputation() async throws -> Int {
var result = 0
for i in 0..<1_000_000 {
try Task.checkCancellation() // throws CancellationError if cancelled
result += i
}
return result
}
Most standard library async APIs check cancellation automatically. Long-running custom loops should call Task.checkCancellation periodically.
Actors — thread-safe shared state
Swift 5.5 also added actors for protecting shared state:
actor BankAccount {
private var balance: Int = 0
func deposit(_ amount: Int) {
balance += amount
}
func getBalance() -> Int {
return balance
}
}
let account = BankAccount()
await account.deposit(100) // await — entering the actor
let b = await account.getBalance() // serialized access
Actors serialize access to their state — only one task can be inside an actor's methods at a time. Race conditions on actor state become impossible.
Common mistakes
- Calling async from sync without a Task — compile error. Wrap in
Task { ... }. - Forgetting
tryfor throwing async calls —try awaitis the common combo. - Awaiting in a loop when independent items could run in parallel — use
async letor TaskGroup. - Nested
Task { }in actor methods unintentionally — the inner Task is detached from the actor's isolation. Useawaitinstead when you want serialized access.
What our grader can actually run
Our Swift grader is Swift 5.2, which predates structured concurrency. async, await, actor and Task are not keywords there, so a file using them does not compile - the parser reads async as a stray identifier and reports consecutive statements on a line must be separated by ';'. Everything above is the code you will write in a current project; it simply cannot be executed on this grader.
So the exercise below reaches for the tool the same runtime already had, Dispatch:
import Foundation
import Dispatch
let inputs = [5, 10, 15]
var results = [Int](repeating: 0, count: inputs.count)
let lock = NSLock()
DispatchQueue.concurrentPerform(iterations: inputs.count) { i in
let doubled = inputs[i] * 2
lock.lock()
results[i] = doubled
lock.unlock()
}
print(results.map(String.init).joined(separator: " "))
// 10 20 30
Worth lining the two models up while they are side by side:
async let and task groups | DispatchQueue.concurrentPerform |
|---|---|
| a waiting task suspends and holds no thread | a waiting task blocks and holds its thread |
results arrive as typed bindings you await | you write into shared storage yourself |
| isolation is checked by the compiler | NSLock is your responsibility, and omitting it is a data race |
That last row is the reason the newer model exists. concurrentPerform hands you the parallelism and nothing else: two closures can reach the same array at the same moment, so indexing by i and taking the lock around the write is what makes the result correct rather than merely usually correct. Take the lock around the whole closure instead and you have re-serialised the work you just parallelised.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…