Index

Structured Concurrency

Structured Concurrency is a set of features to augment concurrency in the Swift language. It provides

  • async/await and async-let.
  • A cooperative thread pool to run tasks with priorities, dependencies, local values, and groups.
  • Sendable: A marker protocol that signifies thread-safe types.
  • Actors: compiler-implemented reference types that ensure *mutual exclusion*.
  • Continuations bridge traditional callbacks with new async functions.

The main difference is that Structured Concurrency uses cooperative multitasking to enforce forward progress.

async/await

An asynchronous function is one that suspends itself while waiting for a slow computation to complete. The key innovation is that suspension does not block the thread, as is typically the case with synchronous functions.

Consider the following example:

func execute(request: URLRequest) async throws -> (Data, URLResponse) {
    try await URLSession.shared.data(for: request, delegate: nil)
}

let request = URLRequest(url: URL(string: "http://foo.com")!)
let (data, response) = try await execute(request: request)

Two aspects are noteworthy:

  • It runs a network request without a completion handler.
  • The function is declared with async and called with await.
// declaration examples
func f() async { ... }
func f() async -> X { ... }
func f() async throws { ... }
func f() async throws -> X { ... }

// call examples
await f()
try await f()
let x = await f()
let x = try await f()

See also:

async-let

An async-let binds the result of an async function but doesn’t await until the result is first referenced.

async let result = f()
// ... execution continues while f is executing
await print(result) // caller suspends awaiting the result

See also: example.

Tasks

Task

A task is a unit of asynchronous work.

Task features:

  • They express child-parent dependencies –this prevents priority inversion.
  • They can be canceled, queried, re-prioritized.
  • They store task-local values that are inherited by child tasks.

Because Task have the concept of child-parent, they are called different things in respect to each other:

Task { ... } // unstructured task

Task.detached { ... } // detached task

let parentTask = Task { // parent task
    Task {} // child task
    Task.detached {} // child detached task
}

parentTask.cancel() // cancels parent and child, but not the detached task

Task eats exceptions

If the code inside a Task throws an exception you won’t receive any warning. If you want to remind yourself to catch errors, you can write this instead:

typealias SafeTask<T> = Task<T, Never>
    
// usage
SafeTask<Void> { 
    // no throwing allowed here
    // if anything throws you must wrap it in a do/catch
}

Detached tasks

A detached task doesn’t inherit anything from the parent task

  • doesn’t inherit priorities
  • doesn’t inherit local task storage
  • it’s not canceled when the parent is

A detached task completes even when there are no references pointing to it.

See also:

TaskGroup

TaskGroup is a container designed to hold an arbitrary number of tasks.

let result = await withTaskGroup(of: Int.self) { group -> Int in    
    group.addTask { ... }
    group.addTask { ... }
    return await group.reduce(0, +)
}
print(result)

When a TaskGroup is executed, all added tasks begin running immediately and in any order. Once the initialization closure exits, all child tasks are implicitly awaited. If a task throws an error, all tasks are canceled, the group waits for them to finish, and then the group throws the original error.

See also: example 1, example 2.

Sendable

Sendable is a marker that indicates types that can be safely transferred across threads. There is a Sendable marker protocol and a @Sendable function attribute.

final class Counter: Sendable { ... }

@Sendable 
func bar() { ... }

See also:

@TaskLocal

A task local value is a Sendable value associated with a task context. They are inherited by the task’s children. When the root task context ends, the value is discarded.

To declare a property as task-local add static and annotate with @TaskLocal:

enum Request {
    @TaskLocal static var id = 0
}

To get/set a task local value

print(Request.id) // 0

Request.$id.withValue(Request.id + 1) {
    print(Request.id) // 1
}

See also:

Actors

An actor is a reference type with mutual exclusion –meaning: only one call to the actor is active at a time. This is also known as actor isolation.

This is implemented using a serial executor which serializes calls from inside or outside the actor. Calls inside the actor run uninterrumpted to their end so they are synchronous. Calls from other types may need to wait for their turn so they must be asynchronous.

nonisolated is a keyword that disables isolation on selected methods and variables. It is allowed as long as nonisolated code doesn’t interact with isolated code.

actor Counter 
{    
    // this is safe to mark nonisolated since it is a constant
    nonisolated let name = "my counter"
    
    var count = 0
    func incrementAge() -> Int {
        age += 1
        return value
    }
}

See also: Priority inversion.

Global actors

A global actor is a singleton actor available globally. It can be used to isolate a whole object, individual properties, or methods.

import Foundation

// declaration of a custom global actor
@globalActor
actor MyActor {
    static let shared = MyActor()
}

// isolation of a whole object
@MyActor
final class SomeClass {
    // opting out on selected members
    nonisolated let foo = "bar"
}

// isolation of individual properties and methods
final class SomeOtherClass {
    @MyActor var state = 0
    @MyActor func foo() {}
}

One such actor is the @MainActor, which is an attribute that indicates code (a function, property, or a whole type) should run on the main thread. This eliminates the guesswork about whether code should run on the main thread.

Running MainActor.run from an async context is the equivalent to GCD’s DispatchQueue.main.async:

Task {
    await MainActor.run {
        // same as DispatchQueue.main.async { .. }
    }
}

In a function annotated @MainActor, any further async calls don’t block the main thread. For instance:

@MainActor
func fetchImage(for url: URL) async throws -> UIImage {
    
    // doesn’t block main
    let (data, _) = try await URLSession.shared.data(from: url) 
    
    // image decompression does block main
    guard let image = UIImage(data: data) else { 
        throw ImageFetchingError.imageDecodingFailed
    }
    return image
}

See also:

AsyncSequence

AsyncSequence is an ordered, asynchronously generated sequence of elements. Basically, a Sequence with an asynchronous iterator.next(). Example, API.

Many library objects now return async sequences:

let url = URL(string: "http://google.com")!
let firstTwoLines = try await url.lines.prefix(2).reduce("", +)

AsyncStream is an asynchronous sequence generated from a closure that calls a continuation to produce new elements. Example, API. Things of note regarding AsyncStream:

  • Iterating over an AsyncStream multiple times, or creating multiple iterators is considered a programmer error.
  • The closure used to create the AsyncStream can’t call async code. If you need to await, use AsyncSequence instead.
  • If your code can throw, use AsyncThrowingStream instead.

Preparing for Swift 6

The article Concurrency in Swift 5 and 6 talks about restrictions that will be applied in Swift 6. To enable some of them pass these flags to the compiler:

OTHER_SWIFT_FLAGS: -Xfrontend -warn-concurrency -Xfrontend -enable-actor-data-race-checks

Or from SPM add this to your target

swiftSettings: [
    .unsafeFlags([
        "-Xfrontend", "-warn-concurrency",
        "-Xfrontend", "-enable-actor-data-race-checks",
    ])
]

Sometimes the solution is straightforward, e.g. use weak self to refer to controllers, e.g. add mutated objects to the capture list, etc. Some errors are a pain in the ass, e.g. Data is not Sendable so you can’t pass it around and it may be too expensive to copy.

These flags may or may not be useful. They

  • reveal some errors that may pass undetected in the default compiler
  • force you to consider edge cases in your code
  • are at times too restrictive to be taken seriously

According to Doug Gregor:

To be very clear, I don’t actually suggest that you use -warn-concurrency in Swift 5.5. It’s both too noisy in some cases and misses other cases. Swift 5.6 brings a model that’s designed to deal with gradual adoption of concurrency checking.

See also: Staging in Sendable checking.

Testing

Not much is new. From XCTestCase:

If your app uses Swift Concurrency, annotate test methods with async or async throws instead to test asynchronous operations, and use standard Swift concurrency patterns in your tests.

For instance

class MyTests: XCTestCase
{
    override func setUp() async throws { ... }
    func testMyCode() async throws { ... }
}

However, class methods seem to be not supported. Using NSLock or os_unfair_lock_lock primitives inside are ignored. I guess you could use a runloop and check for a side effect but it’s too ugly.

// not supported
override class func setUp() async throws { ... }

See also