Concurrency
Swift 5.5 (2021) introduced structured concurrency — async/await for asynchronous functions, Task for concurrent execution, TaskGroup for parallel work, and actor for isolated mutable state. Swift 6 (2024) introduced strict concurrency checking by default — admits substantial compile-time data-race protection through the Sendable protocol. The conventional contemporary form: async functions return values via suspension points (await); Tasks admit launching concurrent work; Actors admit isolating mutable state from data races; AsyncSequence admits asynchronous iteration. The combination — async/await, structured tasks, actors for isolation, the Sendable protocol for thread-safety, the strict concurrency checking — is the substance of Swift’s concurrency model.
async and await
A function marked async admits suspension points:
func fetchData(from url: URL) async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// Calling:
let data = try await fetchData(from: url)
The await admits the runtime suspending the current task at this point — the calling thread is freed to do other work. When the awaited operation completes, the task resumes (possibly on a different thread).
async let
For concurrent execution of independent work:
func loadAll() async throws -> (User, [Post], [Comment]) {
async let user = fetchUser() // starts immediately
async let posts = fetchPosts() // starts immediately
async let comments = fetchComments() // starts immediately
return try await (user, posts, comments) // wait for all three
}
The async let admits parallel execution of independent operations — substantial concurrency without explicit task management.
Task
A Task admits launching a unit of asynchronous work:
Task {
do {
let data = try await fetch()
process(data)
} catch {
log(error)
}
}
// Detached (not inheriting actor context):
Task.detached {
/* runs without inheriting the caller's actor context */
}
// Capturing return value:
let task = Task { () -> Int in
try await fetchAndCompute()
}
let result = await task.value // waits for completion
Tasks are structured by default — they inherit the priority and task-local values of their creator.
For tasks that should not inherit:
Task.detached(priority: .background) {
/* explicit, doesn't inherit */
}
TaskGroup
For substantial parallel work with substantial collection:
func fetchAll(urls: [URL]) async throws -> [Data] {
try await withThrowingTaskGroup(of: Data.self) { group in
for url in urls {
group.addTask { try await fetch(url) }
}
var results: [Data] = []
for try await data in group { // collect as they complete
results.append(data)
}
return results
}
}
The withThrowingTaskGroup admits structured parallelism — all child tasks complete before the parent returns; on error, sibling tasks are cancelled.
For non-throwing version:
let results = await withTaskGroup(of: Int.self) { group in
for i in 0..<10 {
group.addTask { compute(i) }
}
var sum = 0
for await value in group {
sum += value
}
return sum
}
Cancellation
Tasks admit cancellation via cooperative checking:
let task = Task {
for i in 0..<1000 {
try Task.checkCancellation() // throws if cancelled
try await processBatch(i)
}
}
// Later:
task.cancel() // signals cancellation
The Task.checkCancellation() throws CancellationError if cancelled.
For non-throwing checking:
let task = Task {
for i in 0..<1000 {
if Task.isCancelled { break }
await processBatch(i)
}
}
The conventional discipline checks for cancellation at substantial work boundaries.
Task.sleep
try await Task.sleep(for: .seconds(2)) // structured sleep
try await Task.sleep(for: .milliseconds(500))
try await Task.sleep(nanoseconds: 1_000_000_000) // legacy
// In a retry pattern:
for attempt in 0..<3 {
do {
return try await fetch()
} catch {
if attempt < 2 {
try await Task.sleep(for: .seconds(pow(2.0, Double(attempt))))
}
}
}
Actors
An actor admits isolating mutable state — only one task at a time may access an actor’s mutable state:
actor Counter {
private var count: Int = 0
func increment() {
count += 1 // safe: actor-isolated
}
func value() -> Int {
count
}
}
let counter = Counter()
await counter.increment() // await — calls cross actor boundary
let n = await counter.value()
Actor methods are implicitly async from outside the actor; non-async from inside (no need for await for self-references).
The mechanism admits substantial protection against data races — the compiler enforces actor isolation.
Multiple readers and writers
Actors admit only one task accessing mutable state at a time. For concurrent readers, the conventional substitute is immutable data or separate read-only views.
Actor isolation
actor BankAccount {
private var balance: Double = 0
func deposit(_ amount: Double) {
balance += amount
}
func withdraw(_ amount: Double) throws {
guard balance >= amount else {
throw AccountError.insufficientFunds
}
balance -= amount
}
func transfer(to other: BankAccount, amount: Double) async throws {
try withdraw(amount) // self-call; not async
await other.deposit(amount) // cross-actor; async
}
}
The cross-actor calls (await other.deposit) admit substantial coordination through the await.
MainActor
The @MainActor admits “this code runs on the main thread”:
@MainActor
class ViewModel {
var items: [Item] = [] // main-actor isolated
func update() {
items.append(Item()) // safe; on main thread
}
}
// Calling from a non-MainActor context:
let vm = ViewModel()
await MainActor.run {
vm.update()
}
// Or with a method:
@MainActor
func updateUI() {
/* UI work */
}
Task {
await updateUI() // hop to main actor
}
The mechanism admits substantial UI thread safety — UI work runs on the main thread; the compiler enforces the contract.
Sendable
The Sendable protocol marks types as safe to share across concurrency boundaries:
struct Point: Sendable { // value-type with Sendable properties — safe
let x: Double
let y: Double
}
actor Counter { // actors are implicitly Sendable
var count: Int = 0
}
class Mutable { // class is NOT Sendable by default
var value: Int = 0
}
// Strict concurrency:
func send(_ value: Mutable) async { // ERROR: Mutable is not Sendable
/* ... */
}
For unchecked Sendable conformance (when the implementation guarantees safety):
final class ImmutableCache: @unchecked Sendable {
let data: [String: Data] // immutable; safe via developer guarantee
init(data: [String: Data]) {
self.data = data
}
}
Swift 6 admits strict concurrency checking by default — the compiler enforces Sendable constraints across concurrency boundaries.
The conventional discipline:
- Use value types for substantial Sendable conformance (most are auto-Sendable).
- Use actors for mutable shared state.
- Use
@MainActorfor UI code. - Use
@unchecked Sendablesparingly — admit only with substantial discipline.
AsyncSequence
The AsyncSequence protocol admits asynchronous iteration:
for try await line in fileHandle.bytes.lines {
process(line)
}
for try await chunk in url.resourceBytes.chunks(ofCount: 1024) {
write(chunk)
}
Custom AsyncSequence:
struct Counter: AsyncSequence {
typealias Element = Int
let max: Int
struct AsyncIterator: AsyncIteratorProtocol {
var current = 0
let max: Int
mutating func next() async -> Int? {
try? await Task.sleep(for: .seconds(1))
guard current < max else { return nil }
defer { current += 1 }
return current
}
}
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(max: max)
}
}
for await n in Counter(max: 5) {
print(n) // 0, 1, 2, 3, 4 (one per second)
}
The conventional alternative is AsyncStream:
let stream = AsyncStream<Int> { continuation in
Task {
for i in 0..<5 {
try await Task.sleep(for: .seconds(1))
continuation.yield(i)
}
continuation.finish()
}
}
for await value in stream {
print(value)
}
The AsyncStream admits substantial integration with closure-based APIs.
Common patterns
Sequential async
func process(items: [Item]) async throws -> [Result] {
var results: [Result] = []
for item in items {
let r = try await processOne(item) // sequential
results.append(r)
}
return results
}
Parallel async
func process(items: [Item]) async throws -> [Result] {
try await withThrowingTaskGroup(of: Result.self) { group in
for item in items {
group.addTask { try await processOne(item) }
}
var results: [Result] = []
for try await r in group {
results.append(r)
}
return results
}
}
Bounded parallelism
func process(items: [Item], concurrency: Int) async throws -> [Result] {
try await withThrowingTaskGroup(of: Result.self) { group in
var results: [Result] = []
var iter = items.makeIterator()
// Initial batch:
for _ in 0..<concurrency {
guard let item = iter.next() else { break }
group.addTask { try await processOne(item) }
}
// Replace as they complete:
while let r = try await group.next() {
results.append(r)
if let item = iter.next() {
group.addTask { try await processOne(item) }
}
}
return results
}
}
Timeout
func withTimeout<T>(_ duration: Duration, _ operation: @escaping () async throws -> T) async throws -> T {
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask { try await operation() }
group.addTask {
try await Task.sleep(for: duration)
throw TimeoutError.exceeded
}
let result = try await group.next()!
group.cancelAll()
return result
}
}
let data = try await withTimeout(.seconds(5)) {
try await fetch()
}
Retry with exponential backoff
func with<T>(retries: Int, _ operation: @escaping () async throws -> T) async throws -> T {
var lastError: Error?
for attempt in 0..<retries {
do {
return try await operation()
} catch {
lastError = error
if attempt < retries - 1 {
try await Task.sleep(for: .seconds(pow(2.0, Double(attempt))))
}
}
}
throw lastError!
}
Cancellation propagation
func process(items: [Item]) async throws -> [Result] {
var results: [Result] = []
for item in items {
try Task.checkCancellation() // check periodically
let r = try await processOne(item)
results.append(r)
}
return results
}
Producer-consumer with AsyncStream
func makeFeed() -> AsyncStream<Event> {
AsyncStream { continuation in
let observer = EventBus.subscribe { event in
continuation.yield(event)
}
continuation.onTermination = { _ in
EventBus.unsubscribe(observer)
}
}
}
let feed = makeFeed()
for await event in feed {
handle(event)
}
Actor for thread-safe state
actor Cache {
private var storage: [String: Data] = [:]
func get(_ key: String) -> Data? {
storage[key]
}
func set(_ key: String, _ value: Data) {
storage[key] = value
}
func clear() {
storage.removeAll()
}
}
let cache = Cache()
await cache.set("key", data)
let retrieved = await cache.get("key")
@MainActor for UI
@MainActor
class TodoViewModel: ObservableObject {
@Published var items: [Todo] = []
@Published var isLoading: Bool = false
func loadItems() async throws {
isLoading = true // main thread
defer { isLoading = false }
let fetched = try await api.fetchTodos()
items = fetched
}
}
Concurrent map
extension Sequence where Element: Sendable {
func concurrentMap<T: Sendable>(_ transform: @Sendable @escaping (Element) async throws -> T) async throws -> [T] {
try await withThrowingTaskGroup(of: (Int, T).self) { group in
for (index, element) in self.enumerated() {
group.addTask { (index, try await transform(element)) }
}
var results = Array<T?>(repeating: nil, count: try await self.reduce(0, { count, _ in count + 1 }))
for try await (index, value) in group {
results[index] = value
}
return results.compactMap { $0 }
}
}
}
Continuations for callback bridging
extension URLSession {
func data(from url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
// legacy callback-based API
}
}
func fetch(from url: URL) async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
URLSession.shared.data(from: url) { result in
switch result {
case .success(let data): continuation.resume(returning: data)
case .failure(let error): continuation.resume(throwing: error)
}
}
}
}
The withCheckedContinuation and withCheckedThrowingContinuation admit substantial bridging from callback-based APIs to async/await.
Task-local values
struct CurrentUser {
@TaskLocal static var current: User?
}
func processRequest(user: User, work: () async throws -> Void) async throws {
try await CurrentUser.$current.withValue(user) {
try await work()
}
}
// Inside the work:
let userID = CurrentUser.current?.id
The mechanism admits substantial context propagation across the async-await tree.
Task for fire-and-forget
class ViewController {
func onTapButton() {
Task {
do {
try await api.submitData()
} catch {
showError(error)
}
}
}
}
Combine integration
For substantial reactive patterns, Combine (Apple’s reactive framework) admits substantial integration with async:
import Combine
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: User.self, decoder: JSONDecoder())
// As async:
for await user in publisher.values {
print(user)
}
// Or a single value:
let user = try await publisher.values.first(where: { _ in true })!
A note on Swift 6 strict concurrency
Swift 6 (2024) admits complete concurrency checking by default:
Sendableconformance is required for cross-boundary sharing.- Actor isolation is enforced.
- Data races are caught at compile time.
For migrating from Swift 5:
// Swift package manifest:
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
]
The conventional contemporary discipline embraces strict concurrency for new code.
A note on the conventional discipline
The contemporary Swift concurrency advice:
- Use
async/awaitfor asynchronous code. - Use
Taskfor launching concurrent work. - Use
TaskGroupfor substantial parallelism. - Use
async letfor independent concurrent work. - Use
actorfor mutable shared state. - Use
@MainActorfor UI code. - Use
Sendable— value types are conventional. - Use cancellation checks at substantial work boundaries.
- Use
Task.sleepfor delays. - Use
withCheckedContinuationfor callback bridging. - Use
AsyncSequencefor streaming data. - Migrate to strict concurrency — Swift 6’s default.
- Use Combine for substantial reactive patterns; async/await otherwise.
The combination — async/await, structured tasks, actors for isolation, Sendable for thread-safety, AsyncSequence for streaming, the strict concurrency checking — is the substance of Swift’s concurrency model. The discipline produces correct, performant, type-safe concurrent code with substantial protection against the conventional data-race pitfalls.