concurrent

Swift has built-in support for writing asynchronous and parallel code in a structured way. Asynchronous code can be suspended and executed later, although only one part of the program is executed at a time. Asynchronously executed code allows programs to perform time-consuming operations such as network requests or parsing files without blocking non-time-consuming tasks such as page updates and user interactions that require quick response. Parallel code means multitasking at the same time — for example, a computer with a quad-core processor can run four pieces of code simultaneously, with each core performing one task. A program that uses parallel and asynchronous code to perform multiple operations at the same time, waiting for an operation from an external system (such as a return from a server on a network request), and making it easier to write such code in a memory-safe manner.

The additional scheduling flexibility of parallel or asynchronous code also comes at the cost of increased complexity. Swift allows you to express intent in a way that is checked at compile time — for example, you can use actors to securely access mutable state. However, adding concurrency to slow or buggy code does not guarantee that it will be fast or correct. In fact, adding concurrency may even make your code harder to debug. However, using Swift’s language-level concurrency support in code that requires concurrency means Swift can help you catch problems at compile time.

The rest of this chapter will use the term “concurrency” to refer to this common combination of asynchronous and parallel code.

If you’ve written concurrent code before, you’re probably used to using threads. The concurrency model in Swift is based on threads, but you can’t interact with them directly. An asynchronous function in Swift can abandon the thread on which it runs, so that when the first function is blocked, another asynchronous function can run on that thread.

Although concurrent code can be written without Swift language support, it is often difficult to read. For example, the following code downloads a list of photo names, downloads the first photo in the list, and displays that photo to the user:

listPhotos(inGallery: "Summer Vacation") { photoNames in
    let sortedNames = photoNames.sorted()
    let name = sortedNames[0]
    downloadPhoto(named: name) { photo in
        show(photo)
    }
}
Copy the code

Even in this simple case, because the code must be written as a series of completion handlers, you end up writing nested closures. In this style, more complex code with deep nesting quickly becomes unmanageable.

Define and call asynchronous functions

An asynchronous function or method is a special type of function or method that can be suspended during execution. This is in contrast to normal synchronous functions and methods, which either run until complete, throw an error, or never return. An asynchronous function or method will still do one of these three things, but it can also pause in the middle while waiting for something. In the body of an asynchronous function or method, mark each place where execution can be suspended.

To indicate that a function or method is asynchronous, you can write the async keyword after its argument in its declaration, similar to using throws to mark a thrown function. If the function or method returns a value, write async before the return arrow (->). For example, here’s how to get the name of a photo in the gallery:

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

For asynchronous and thrown functions or methods, write async before throwing. When an asynchronous method is called, execution hangs until the method returns. You write await before the call to mark a possible pause point. This is like writing a try when a throw function is called and marking possible changes to the program flow when an error occurs. Within an asynchronous method, the flow of execution hangs only if it calls another asynchronous method — a hang is never implicit or pre-emptive — which means that every possible hang point is marked with await. For example, the following code gets the names of all the images in a gallery and displays the first image:

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

Because listPhotos(inGallery:) and downloadPhoto(named:) functions both make network requests, they can take quite a long time to complete. Make async asynchronous by writing them before the return arrow, and let the rest of the application continue to run while it waits for the image to be ready. To understand the concurrency nature of the above example, here is a possible order of execution:

  1. The code runs from the first line to the first await line. It calls the listPhotos(inGallery:) function and pauses execution while it waits for the function to return.
  2. When the execution of this code is suspended, some other concurrent code in the same program runs. For example, maybe a long-running background task continues to update a list of new photo libraries. The code also runs until the next hang point (marked as await), or until it completes.
  3. After listPhotos(inGallery:) returns, the code will continue executing from there. It assigns the returned value to the photoNames.
  4. The line defining sortedNames and names is regular synchronization code. Because there are no waiting marks on these lines, there cannot be any hanging points.
  5. The next await marks a call to the downloadPhoto(named 🙂 function. This code again pauses execution until the function returns, giving other concurrent code a chance to run.
  6. After downloadPhoto(named:) returns, its return value is assigned to photo and then passed as an argument in the call to show(_:).

A possible hang starting point marked with await in code indicates that the current section of code may suspend execution while waiting for the return of an asynchronous function or method. This is also known as a yield thread, because behind the scenes, Swift suspends code execution on the current thread and runs other code on that thread. Because code with await needs to be able to suspend execution, only certain places in the 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 marked with @main.
  • The code in the independent subtask is shown in the following unstructured concurrency:

The task.sleep (_:) method is useful when writing simple code to understand how concurrency works. This method does nothing, but waits at least a given number of nanoseconds before returning. Here’s a version of the listPhotos(inGallery:) function that uses sleep() to simulate waiting for network operations:

func listPhotos(inGallery name: String) async -> [String] {
    await Task.sleep(2 * 1_000_000_000)  // Two seconds
    return ["IMG001", "IMG99", "IMG0404"]
}
Copy the code

Asynchronous sequence

ListPhotos (inGallery:) after all the elements of the array are ready, the function in the previous section returns the entire array asynchronously. Another approach is to use an asynchronous sequence to wait for one element of the collection at a time. Here’s what iterating over an asynchronous sequence looks like:

import Foundation

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

Instead of using a normal for-in ring, the example written above has await for. Just as when calling an asynchronous function or method, writing await represents a possible pause point. A for-await -in loop may be paused at the beginning of each iteration while it waits for the next element. In the same way, you can use your own type for-in by adding the loop-sequence protocol, and you can use your own type for-await-in by adding the loop-AsyncSequence protocol.

Call asynchronous functions in parallel

Calling the asynchronous function await runs only one piece of code at a time. When the asynchronous code runs, the caller waits for it to complete before proceeding to the next line of code. For example, to get the first three photos from the gallery, you can wait for three calls to this downloadPhoto(named:) function, as shown below:

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

let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
Copy the code

This approach has an important drawback: while the download is asynchronous and allows other work to occur during the download, only one call is run at a time by downloadPhoto(named:). Each photo will be fully downloaded before the next one starts downloading. But there’s no wait — each photo can be downloaded independently, or even at the same time. To call an asynchronous function and have it run in parallel with the code around it, async writes first when let defines a constant, and await writes each time the constant is used.

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)
Copy the code

In this example, all three calls start with downloadPhoto(named:) without waiting for the previous call to complete. They can run simultaneously if sufficient system resources are available. These function calls are unmarked, await, because the code does not hang to wait for the result of the function. Instead, execution continues up to the line where photos is defined — at this point, the program needs the result of these asynchronous calls, so you write await to pause execution until all three photos have been downloaded. Here’s how you think about the difference between the two approaches:

  • Call the asynchronous function when the code in the following line depends on the result of the function. This creates the work to be performed sequentially.
  • Use async- call the asynchronous function let when you need the results later in the code. This creates work that can be performed in parallel.
  • Both await and async-let allow other code to run when they are paused.
  • In both cases, you can indicate that execution will be suspended (if necessary) until the asynchronous function returns by marking a possible pause point await.

You can also mix these two approaches in the same code.

Tasks and task groups

A task is a unit of work that can be run asynchronously as part of a program. All asynchronous code runs as part of some task. The async-let syntax described in the previous section creates a subtask for you. You can also create a task group and add subtasks to that group, which gives you more control over prioritization and cancellation, and allows you to create a dynamic number of tasks.

Tasks are arranged in a hierarchy. Each task in a task group has the same parent task, and each task can have subtasks. Because there is an explicit relationship between tasks and task groups, this approach is called structured concurrency. Although you take some responsibility for correctness, the explicit parent-child relationship between tasks lets Swift handle some behavior, such as propagating cancellations for you, and lets Swift detect some errors at compile time.

await withTaskGroup(of: Data.self) { taskGroup in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    for name in photoNames {
        taskGroup.async { await downloadPhoto(named: name) }
    }
}
Copy the code

Unstructured concurrency

In addition to the structured concurrency approach described in the previous section, Swift also supports unstructured concurrency. Unlike tasks that are part of a task group, unstructured tasks have no parent task. You have complete flexibility to manage unstructured tasks in any way your program needs, but you are also fully responsible for their correctness. To create an unstructured Task running in the current actor, call the task.init (Priority: Operation 🙂 initializer. To create an unstructured Task that is not part of the current participant, more specifically called a detached Task, call the task.detached (priority: Operation 🙂 class method. Both operations return a handle to the task, allowing you to interact with the task — for example, to wait for its result or cancel it.

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

Task to cancel

Swift concurrent use of the collaboration cancellation model. Each task checks whether it was cancelled at the appropriate point in its execution and responds to cancellation in any appropriate way. Depending on what you do, this usually means one of the following:

  • Throws an error, such as CancellationError
  • Returns nil or an empty set
  • Return work partially completed

To check for cancellation, call task.checkCancellation (), CancellationError thrown if the Task has been cancelled, or check for the value task.iscancelled and handle the cancellation in your own code. For example, the task of downloading photos from a gallery might require removing part of the download and shutting down your network connection.

To propagate cancellation manually, call task.cancel ().

Actor

Actors, like Classes, Are Reference Types, so Classes Are Reference Types where comparisons between value Types and Reference Types apply to both actors and Classes. Unlike a class, an actor allows only one task to access its mutable state at a time, which makes it safe for code in multiple tasks to interact with the same actor instance.

For example, here is an actor that records temperature:

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
    }
}
Copy the code

You introduce an actor with the actor keyword and define it in a pair of curly braces. The TemperatureLogger actor has features that other code actors can enter outside, and limits the Max attribute so that only the code inside the actor can be updated to the maximum.

You can create an instance of actor using the same initializer syntax as for structures and classes. When you access actor properties or methods, you flag potential pause points with await — for example:

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

In this case, accessing logger.max is a possible pause point. Because actors only allow one task to access its mutable state at a time, if code from another task has already interacted with the logger, that code is paused while waiting to access the property.

In contrast, await accesses the actor’s properties without writing part of the actor’s code. For example, here’s how a TemperatureLogger updates A with a new temperature:

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

The update(with:) method is already running on actor, so it does not mark its access to await properties as maxwith does. This method also shows one reason why actors only allow one task at a time to interact with their mutable state: some updates to the Actor’s state temporarily break the invariants. The TemperatureLogger actor tracks down the list and maximum temperatures, and updates its maximum temperatures in recording new measurements. During the update process, the temperature recorder is in a temporarily inconsistent state after the new measurement is appending but before the update Max. Preventing multiple tasks from interacting with the same instance at the same time prevents problems such as the following sequence of events:

  1. Your code calls the update(with:) method. It first updates the measurements array.

  2. Before your code can update Max, the code elsewhere reads the maximum and temperature array.

  3. Your code by changing Max

In this case, code running elsewhere will read incorrect information because its access to the actor is interleaved between calls, with update(with:) and data temporarily invalid. You can prevent this problem when using Swift Actors because they allow only one operation on their state at a time, and because the code can only break where the await marks the pause point. Because update(with:) does not contain any pause points, no other code can access the data during the update.

If you try to access these properties from outside the actor, as with an instance of the class, you will receive a compile-time error; Such as:

print(logger.max)  // Error
Copy the code

Logger.max fails to access the await without writing because the actor’s properties are part of the isolated local state of the actor. Swift guarantees that only code inside the actor can access the actor’s local state. This assurance is called participant isolation.