Skip to content

Swift Concurrency

The concurrency model in Swift is built on top of threads, but you don’t interact with them directly.

An asynchronous function in Swift can give up the thread that it’s running on, which lets another asynchronous function run on that thread while the first function is blocked. When an asynchronous function resumes, Swift doesn’t make any guarantee about which thread that function will run on.

It’s possible to write concurrent code without using Swift’s language support.

async/await

Ordinary, synchronous functions and methods either run to completion, throw an error, or never return.

An asynchronous function or asynchronous method still does one of those three things, but it can also pause in the middle when it’s waiting for something.

Inside the body of an asynchronous function or method, you mark each of these places where execution can be suspended.

You write the async keyword in its declaration after parameters, before throws if have and before the return arrow (->) to indicate that a function or method is asynchronous.

swift
func listPhotos(inGallery name: String) async -> [String] {
    let result = // ... some asynchronous networking code ...
    return result
}

When calling an asynchronous method, execution suspends until that method returns. You write await in front of the call to mark the possible suspension point.

swift
let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)

Because code with await needs to be able to suspend execution, only certain places in your program can call asynchronous functions or methods:

  • Code in the body of an asynchronous function, method, or property.
  • Code in the static main() method of a structure, class, or enumeration that’s marked with @main.
  • Code in an unstructured child task.

When adding concurrent code to an existing project, work from the top down. Specifically, start by converting the top-most layer of code to use concurrency, and then start converting the functions and methods that it calls, working through the project’s architecture one layer at a time. There’s no way to take a bottom-up approach, because synchronous code can’t ever call asynchronous code.

completionHandler vs async/await

completionHandler style function

swift
func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) {
    let request = thumbnailURLRequest(for: id)
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        if error = error {
            completion(nil, error)
        } else if (response as? HTTPURLResponse)?.statusCode != 200 {
            completion(nil, FetchError.badID)
        } else {
            guard let image = UIImage(data, data!) else {
                completion(nil, FetchError.badImage)
                return
            }
            image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
                guard let thumbnail = thumbnail else {
                    completion(nil, FetchError.badImage)
                    return
                }
                completion(thumbnail, nil)
            }
        }
    }
    task.resume()
}

async/await style function

swift
func fetchThumbnail(for id: String) async throws -> UIImage {
    let request = thumbnailURLRequest(for: id)
    let (data, response) = try await URLSession.shared.data(from: request)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else {throw FetchError.badID}
    let maybeImage = UIImage(data: data)
    guard let thumbnail = await maybeImage?.thumbnail else {throw FetchError.badImage}
    return thumbnail
}

extension UIImage {
    var thumbnail: UIImage? {
        get async {
            let size = CGSize(width: 40, height: 40)
            return await self.byPreparingThumbnail(ofSize: size)
        }
    }
}

Only read-only properties can be async.

completionHandler style testcase

swift
class MockViewModelSpec: XCTestCase {
    func testFetchThumbnails() throws {
        let expectation = XCTestExpectation(description: "mock thumbnails completion")
        self.mockViewModel.fetchThumbnail(for: mockID) { result, error in
            XCTAssertEqual(result?.size, CGSize(width: 40, height: 40))
            expectation.fulfill()
        }
        wait(for: [expectation], timeout: 5.0)
    }
}

async/await style testcase.

swift
class MockViewModelSpec: XCTestCase {
    func testFetchThumbnails() async throws {
        let result = try await self.mockViewModel.fetchThumbnail(for: mockID)
        XCTAssertEqual(result.size, CGSize(width: 40, height: 40))
    }
}

continuation

Continuation with completionHandler

swift
func persistentPosts() async throws -> [Post] {
    typealias PostContinuation = CheckedContinuation<[Post], Error>
    return try await withCheckedContinuation { (continuation: PostContinuation) in
        self.getPersistentPosts {posts, error in
            if let error = error {
                continuation.resume(throwing: error)
            } else {
                continuation.resume(returning: posts)
            }
        }
    }
}

Continuation with delegate

swift
class ViewController: UIViewController {
    private var activeContinuation: CheckedContinuation<[Post], Error>?
    func sharedPostsFromPeer() async throws -> [Post] {
        try await withCheckedThrowingContinuation { continuaiton in
            self.activeContinuation = continuaiton
            self.peerManager.syncSharedPosts()
        }
    }
}

extension ViewController: PeerSyncDelegate {
    func peerManager(_ manager: PeerManager, received posts: [Post]) {
        self.activeContinuation?.resume(returning: posts)
        self.activeContinuation = nil // guard against multiple calls to resume
    }
    
    func peerManager( _manager: PeerManager, hadError error: Error) {
        self.activeContinuation?.resume(throwing: error)
        self.activeContinuation = nil // guard against multiple calls to resume
    }
}

Checked continuations:

  1. Continuation must be resumed exactly once on every path.
  2. Discarding the continuation without resuming is not allowd.

for-await-in loop

A for-await-in loop iterates over an asynchronous sequence looks like:

swift
import Foundation

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

async-let

To call an asynchronous function and let it run in parallel with code around it, write async in front of let when you define a constant, and then write await each time you use the constant.

swift
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])

let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
  1. async-let indicates that you don’t need the result until later in your code. This creates work that can be carried out in parallel.
  2. async-let allow other code to run while they’re suspended.
  3. await indicates that execution will pause, if needed, until all asynchronous functions have returned.

task

A task is a unit of work that can be run asynchronously as part of your program. A task itself does only one thing at a time, but when you create multiple tasks, Swift can schedule them to run simultaneously.

The async-let syntax described in the previous section implicitly creates a child task.

structured

Tasks are arranged in a hierarchy. Each task in a given task group has the same parent task, and each task can have child tasks.

  • In a parent task, you can’t forget to wait for its child tasks to complete.
  • When setting a higher priority on a child task, the parent task’s priority is automatically escalated.
  • When a parent task is canceled, each of its child tasks is also automatically canceled.
  • Task-local values propagate to child tasks efficiently and automatically.
swift
let photos = await withTaskGroup(of: Optional<Data>.self) { group in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    for name in photoNames {
        let added = group.addTaskUnlessCancelled {
            guard !Task.isCancelled else { return nil }
            return await downloadPhoto(named: name)
        }
        guard added else { break }
    }


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

unstructured

swift
let newPhoto = // ... some photo data ...
let handle = Task {
    return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.value

actor

Actors allow only one task to access their mutable state at a time.

You introduce an actor with the actor keyword, followed by its definition in a pair of braces.

swift
actor TemperatureLogger {
    let label: String
    var measurements: [Int]
    private(set) var max: Int


    init(label: String, measurement: Int) {
        self.label = label
        self.measurements = [measurement]
        self.max = measurement
    }
}

When you access a property or method of an actor, you use await to mark the potential suspension point. For example:

swift
let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// Prints "25"

If code from another task is already interacting with the logger, this code suspends while it waits to access the property.

Code that’s part of the actor doesn’t write await when accessing the actor’s properties.

swift
extension TemperatureLogger {
    func update(with measurement: Int) {
        measurements.append(measurement)
        if measurement > max {
            max = measurement
        }
    }
}

If code outside the actor tries to access those properties directly, like accessing a structure or class’s properties, you’ll get a compile-time error. For example:

swift
print(logger.max)  // Error

Swift guarantees that only code running on an actor can access that actor’s local state. This guarantee is known as actor isolation.

  • Code in between possible suspension points runs sequentially, without the possibility of interruption from other concurrent code.
  • Code that interacts with an actor’s local state runs only on that actor.
  • An actor runs only one piece of code at a time.

Sendable

A sendable type is a type that can be shared from one concurrency domain to another.

For example, it can be passed as an argument when calling an actor method or be returned as the result of a task.

A class that contains mutable properties and doesn’t serialize access to those properties can produce unpredictable and incorrect results when you pass instances of that class between different tasks.

You mark a type as being sendable by declaring conformance to the Sendable protocol.

In general, there are three ways for a type to be sendable:

  • The type is a value type, and its mutable state is made up of other sendable data.
  • The type doesn’t have any mutable state, and its immutable state is made up of other sendable data.
  • The type has code that ensures the safety of its mutable state, like a class that’s marked @MainActor or a class that serializes access to its properties on a particular thread or queue.

Some types are always sendable, like structures that have only sendable properties and enumerations that have only sendable associated values.

A implicitly sendable type

swift
struct TemperatureReading {
    var measurement: Int
}

A explicitly sendable type

swift
struct TemperatureReading: Sendable {
    var measurement: Int
}

To explicitly mark a type as not being sendable

swift
struct FileDescriptor {
    let rawValue: CInt
}

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

In the code above, the FileDescriptor is a structure that meets the criteria to be implicitly sendable.

However, the extension makes its conformance to Sendable unavailable, preventing the type from being sendable.

References