Structured concurrency is a programming paradigm aimed at improving multithreaded code’s clarity, efficiency, and robustness. It’s a fresh approach that allows you to manage concurrent tasks in a predictable, logical structure. Hence, “structured” concurrency.

What is Structured Concurrency?

Structured concurrency fundamentally means that the lifetime of concurrent tasks is bound and managed. In essence, when a block of code starts a concurrent task, that task is ensured to finish before the block of code concludes. This intrinsic tying of task lifetimes leads to code that is easier to reason about, more predictable, and less prone to bugs.

Swift Concurrency: Before and After

Before structured concurrency, we handled async tasks with callbacks or futures/promises. But these approaches led to a dreaded “callback hell” situation, where callbacks are nested within callbacks, making the code hard to follow and maintain. Swift’s new concurrency model aims to alleviate this by introducing async functions and actors that can be waited on using the await keyword. Here’s a before-and-after comparison:

// Traditional Swift Concurrency (Before)
DispatchQueue.global().async {
    let url = URL(string: "https://api.github.com")!
    let data = try? Data(contentsOf: url)
    DispatchQueue.main.async {
        // Update UI
    }
}

// Structured Concurrency (After)
Task {
    let url = URL(string: "https://api.github.com")!
    let (data, _) = try? await URLSession.shared.data(from: url)
    // Update UI
}

Tasks: The Basic Unit of Computation

In the structured concurrency model, a task is the basic unit of computation. When you start an operation that may take some time, you create a new task. Tasks can be async functions or closures that are marked with the async keyword.

let myTask = Task {
    let url = URL(string: "https://api.github.com")!
    let (data, _) = try! await URLSession.shared.data(from: url)
    // Process data
}

Child Tasks: Tying Lifecycles Together

Tasks can create child tasks, which allows for the creation of a hierarchy of tasks. This hierarchy forms the “structure” in structured concurrency. Child tasks are cancelled when their parent task is cancelled, ensuring a clean shutdown that prevents loose ends.

let myTask = Task {
    await childTask()
    // childTask is cancelled if myTask is cancelled
}

Task Groups: Running Tasks Concurrently

Swift also introduces the concept of task groups, which are a way to run multiple child tasks concurrently. They are a powerful tool that allows you to manage a group of tasks as one unit.

await withTaskGroup(of: Int.self) { group in
    for i in 1...5 {
        group.addTask {
            return i * 2
        }
    }
    // All tasks run concurrently
}

Error Handling: Safer Concurrent Operations

Structured concurrency integrates with Swift’s existing error handling. Errors thrown by async functions can be caught and handled, ensuring that operations can fail safely.

do {
    try await performTask()
} catch {
    print("Error occurred: \(error)")
}

Cancelling

In structured concurrency, you can cancel tasks at any time. This is particularly useful when a task is no longer needed or is taking too long. The ability to cancel tasks gives you finer control over the execution of your program.

let myTask = Task {
    // Perform long running operation
}

myTask.cancel()  // Cancel the task

Continuations: Interoperability with Callback-Based APIs

Swift’s structured concurrency offers continuations to smoothly integrate with callback-based APIs. Continuations provide a way to “suspend” an async function until a callback is invoked, at which point the function can be “resumed”.

func fetchData() async throws -> Data {
    return await withUnsafeContinuation { continuation in
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            let data = "Hello, world!".data(using: .utf8)!
			  continuation.resume(returning: data)
        }
    }
}

Detached Tasks: For Operations Outside the Task Hierarchy

For tasks that don’t logically belong to the current task hierarchy, Swift provides a way to create detached tasks. These are tasks that are not tied to the lifecycle of the creating task.

let myTask = Task.detached {
    // Perform operation that is not tied to the lifecycle of the current task
}

Actors: Safe and Exclusive Data Access

Actors are a new kind of Swift type that can protect access to its own internal state, thus eliminating common data races. It allows data access to be exclusive by only letting one task access mutable actor state at a time.

actor MyActor {
    private var value = 0

    func updateValue(newValue: Int) {
        value = newValue
    }
}

Async/Await: The Cornerstone of Swift’s New Concurrency Model

The async/await syntax is a key part of Swift’s new structured concurrency model. It allows for async functions to be written in a way that looks synchronous, resulting in code that’s easier to read and understand.

await fetchData()  // Call async function

Conclusion

With the introduction of structured concurrency, Swift has taken a significant step forward in making concurrent programming easier, safer, and more efficient. In this blog post, we’ve touched on the main concepts of structured concurrency. In the next post, we’ll delve deeper into tasks, showing you how they can be used to create more complex concurrent behaviour.