Skip to content

Concurrency

Swift offers built-in tools for writing asynchronous and parallel code in a clear, structured manner. Asynchronous code can pause and resume, but only one part runs at once. This allows your app to handle quick tasks, like UI updates, while working on slower ones, such as loading data from the internet or processing files. Parallel code runs multiple operations simultaneously—for instance, a multi-core device can execute several tasks across its cores. Programs often combine these for concurrency, handling multiple tasks while pausing those waiting on external resources.

Concurrency adds flexibility but increases complexity. You can't predict which code runs together or in what order, risking data races—when multiple parts access shared changeable data simultaneously. Swift's concurrency features detect and prevent most data races at compile time, with runtime checks halting others. Use actors and isolation, covered later, to safeguard data.

Note: If you're familiar with threads, Swift's concurrency builds on them but hides direct interaction. Async functions can yield threads, allowing others to run without guaranteeing resumption on the same thread.

Without Swift's concurrency tools, code can be hard to follow. For example, this fetches a list of image names, grabs the first image, and displays it:

swift
fetchImages(fromAlbum: "Family Trip") { imageNames in
    let sortedNames = imageNames.sorted()
    let name = sortedNames[0]
    loadImage(named: name) { image in
        display(image)
    }
}

Nested closures make complex code messy.

Defining and Calling Asynchronous Functions

An asynchronous function or method can pause mid-execution. Unlike synchronous ones, which complete, error, or loop forever, async ones do the same but can suspend while waiting.

Mark a function async with async after parameters (before -> for returns). For throwing async functions, place async before throws.

Example: Fetching image names from an album:

swift
func fetchImages(fromAlbum name: String) async -> [String] {
    let result = // ... async network code ...
    return result
}

Call async functions with await to mark suspension points—similar to try for errors. Execution pauses only on async calls; it's explicit, not automatic.

Example: Fetch and display the first image:

swift
let imageNames = await fetchImages(fromAlbum: "Family Trip")
let sortedNames = imageNames.sorted()
let name = sortedNames[0]
let image = await loadImage(named: name)
display(image)

Possible execution flow:

  1. Run to first await, call fetchImages, suspend.
  2. Other code runs (e.g., background tasks).
  3. Resume after return, assign to imageNames.
  4. Sort synchronously (no await).
  5. Suspend on loadImage.
  6. Resume, display image.

Async code requires specific contexts: async bodies, @main static methods, or unstructured tasks (later).

Use Task.sleep(for:) for testing delays:

swift
func fetchImages(fromAlbum name: String) async throws -> [String] {
    try await Task.sleep(for: .seconds(2))
    return ["IMG001", "IMG99", "IMG0404"]
}

Call with try await. Async functions resemble throwing ones but can't be wrapped synchronously—convert top-down when adding concurrency.

Asynchronous Sequences

Instead of awaiting full collections, use asynchronous sequences to handle elements one-by-one.

Example: Reading input lines async:

swift
import Foundation

let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
    print(line)
}

Use for try await for potential suspensions per iteration. Conform to AsyncSequence like Sequence for sync loops.

Calling Asynchronous Functions in Parallel

Sequential awaits run one-at-a-time:

swift
let firstImage = await loadImage(named: imageNames[0])
let secondImage = await loadImage(named: imageNames[1])
let thirdImage = await loadImage(named: imageNames[2])

let images = [firstImage, secondImage, thirdImage]
display(images)

For parallelism, use async let:

swift
async let firstImage = loadImage(named: imageNames[0])
async let secondImage = loadImage(named: imageNames[1])
async let thirdImage = loadImage(named: imageNames[2])

let images = await [firstImage, secondImage, thirdImage]
display(images)

Starts without waiting; awaits results later. Mix with sequential awaits as needed.

Tasks and Task Groups

A task is async work. All async code runs in tasks, which can run concurrently but sequentially inside.

async let creates implicit child tasks. For dynamic control, use TaskGroup:

swift
await withTaskGroup(of: Data.self) { group in
    let imageNames = await fetchImages(fromAlbum: "Family Trip")
    for name in imageNames {
        group.addTask {
            await loadImage(named: name)
        }
    }

    for await image in group {
        display(image)
    }
}

Creates children for each image; displays as ready (order unpredictable).

For results:

swift
let images = await withTaskGroup(of: Data.self) { group in
    let imageNames = await fetchImages(fromAlbum: "Family Trip")
    for name in imageNames {
        group.addTask {
            await loadImage(named: name)
        }
    }

    var results: [Data] = []
    for await image in group {
        results.append(image)
    }

    return results
}

Structured concurrency ensures parents wait for children, escalates priorities, propagates cancellation, and shares values.

Task Cancellation

Tasks check cancellation cooperatively (e.g., throw CancellationError, return nil).

Use addTaskUnlessCancelled, check Task.isCancelled:

swift
let images = await withTaskGroup { group in
    let imageNames = await fetchImages(fromAlbum: "Family Trip")
    for name in imageNames {
        let added = group.addTaskUnlessCancelled {
            Task.isCancelled ? nil : await loadImage(named: name)
        }
        guard added else { break }
    }

    var results: [Data] = []
    for await image in group {
        if let image { results.append(image) }
    }
    return results
}

Handles partial results on cancel.

For immediate notification:

swift
let task = await Task.withTaskCancellationHandler {
    // ...
} onCancel: {
    print("Canceled!")
}

// Later...
task.cancel()  // Prints "Canceled!"

Unstructured Concurrency

Unstructured tasks lack parents. Use Task { ... } (inherits context) or Task.detached { ... } (independent):

swift
let newImage = // ... image data ...
let handle = Task {
    await add(newImage, toAlbum: "Spring Outing")
}
let result = await handle.value

Manage manually for correctness.

Isolation

Protect shared mutable data via data isolation: immutable data, single-task references, or actor-protected access.

The Main Actor

The main actor safeguards UI data. Pre-concurrency, all runs on it; move heavy work off.

Mark with @MainActor:

swift
@MainActor
func display(_: Data) {
    // ... UI display code ...
}

Call with await off-main:

swift
func loadAndDisplayImage(named name: String) async {
    let image = await loadImage(named: name)
    await display(image)
}

For closures: @MainActor in { ... }.

For types:

swift
@MainActor
struct ImageAlbum {
    var imageNames: [String]
    func renderUI() { /* ... UI code ... */ }
}

Or per-property/method for granularity.

Frameworks often imply @MainActor via protocols/classes/wrappers.

Actors

Define custom actors for safe sharing:

swift
actor TempRecorder {
    let name: String
    var readings: [Int]
    private(set) var highest: Int

    init(name: String, reading: Int) {
        self.name = name
        self.readings = [reading]
        self.highest = reading
    }
}

Access with await:

swift
let recorder = TempRecorder(name: "Outdoor", reading: 25)
print(await recorder.highest)  // "25"

Inside actor, no await needed:

swift
extension TempRecorder {
    func add(with reading: Int) {
        readings.append(reading)
        if reading > highest {
            highest = reading
        }
    }
}

Prevents races by serializing access. Direct external writes error.

Synchronous methods ensure no suspensions, protecting invariants:

swift
extension TempRecorder {
    func convertToCelsius() {
        for i in readings.indices {
            readings[i] = (readings[i] - 32) * 5 / 9
        }
    }
}

Global Actors

Main actor is a @globalActor singleton. Define custom ones similarly.

Sendable Types

Shareable types conform to Sendable. Ways: value types with sendable state, immutable types, or safely managed mutable state.

Example:

swift
struct TempValue: Sendable {
    var value: Int
}

extension TempRecorder {
    func record(from value: TempValue) {
        readings.append(value.value)
    }
}

let recorder = TempRecorder(name: "Tea Pot", reading: 85)
let value = TempValue(value: 45)
await recorder.record(from: value)

Implicit for non-public structs. Mark unavailable to suppress:

swift
struct FileHandle {
    let id: Int
}

@available(*, unavailable)
extension FileHandle: Sendable {}

Released under the MIT License.