An Overview of Swift Structured Concurrency

Concurrency can be defined as the ability of software to perform multiple tasks in parallel. Many app development projects will need to make use of concurrent processing at some point and concurrency is essential for providing a good user experience. Concurrency, for example, is what allows the user interface of an app to remain responsive while performing background tasks such as downloading images or processing data.

In this chapter, we will explore the structured concurrency features of the Swift programming language and explain how these can be used to add multi-tasking support to your app projects.

An Overview of Threads

Threads are a feature of modern CPUs and provide the foundation of concurrency in any multitasking operating system. Although modern CPUs can run large numbers of threads, the actual number of threads that can be run in parallel at any one time is limited by the number of CPU cores (depending on the CPU model, this will typically be between 4 and 16 cores). When more threads are required than there are CPU cores, the operating system performs thread scheduling to decide how the execution of these threads is to be shared between the available cores.

Threads can be thought of as mini-processes running within a main process, the purpose of which is to enable at least the appearance of parallel execution paths within application code. The good news is that although structured concurrency uses threads behind the scenes, it handles all of the complexity for you and you should never need to interact with them directly.

The Application Main Thread

When an app is first started, the runtime system will typically create a single thread in which the app will run by default. This thread is generally referred to as the main thread. The primary role of the main thread is to handle the user interface in terms of UI layout rendering, event handling, and user interaction with views in the user interface.

 

You are reading a sample chapter from SwiftUI Essentials – iOS 16 Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 64 chapters and over 560 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Any additional code within an app that performs a time-consuming task using the main thread will cause the entire application to appear to lock up until the task is completed. This can be avoided by launching the tasks to be performed in separate threads, allowing the main thread to continue unhindered with other tasks.

Completion Handlers

As outlined in the chapter entitled “Swift Functions, Methods and Closures”, Swift previously used completion handlers to implement asynchronous code execution. In this scenario, an asynchronous task would be started and a completion handler assigned to be called when the task finishes. In the meantime, the main app code would continue to run while the asynchronous task is performed in the background. On completion of the asynchronous task, the completion handler would be called and passed any results. The body of the completion handler would then execute and process those results.

Unfortunately, completion handlers tend to result in complex and error-prone code constructs that are difficult to write and understand. Completion handlers are also unsuited to handling errors thrown by the asynchronous tasks and generally result in large and confusing nested code structures.

Structured Concurrency

Structured concurrency was introduced into the Swift language with Swift version 5.5 to make it easier for app developers to implement concurrent execution safely, and in a way that is logical and easy to both write and understand. In other words, structured concurrency code can be read from top to bottom without having to jump back to completion handler code to understand the logic flow. Structured concurrency also makes it easier to handle errors thrown by asynchronous functions.

Swift provides several options for implementing structured concurrency, each of which will be introduced in this chapter.

 

You are reading a sample chapter from SwiftUI Essentials – iOS 16 Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 64 chapters and over 560 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Preparing the Project

Launch Xcode and select the option to create a new Multiplatform App project named ConcurrencyDemo. Once created, edit the ContentView.swift file so that it reads as follows:

import SwiftUI
 
struct ContentView: View {
    var body: some View {
        Button(action: {
            doSomething()
        }) {
            Text("Do Something")
        }
    }
    
    func doSomething() {
    }
 
    func takesTooLong() {
    }    
}
 
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

The above changes create a Button view configured to call the doSomething() function when clicked. In the remainder of this chapter, we will make changes to this template code to demonstrate structured concurrency in Swift.

Non-Concurrent Code

Before exploring concurrency, we will first look at an example of non-concurrent code (also referred to as synchronous code) execution. Begin by adding the following code to the two stub functions. The changes to the doSomething() function print out the current date and time before calling the takesTooLong() function. Finally, the date and time are output once again before the doSomething() function exits.

The takesTooLong() function uses the sleep() method of the Thread object to simulate the effect of performing a time consuming task that blocks the main thread until it is complete before printing out another timestamp:

func doSomething() {
    print("Start \(Date())")
    takesTooLong()
    print("End \(Date())")
}
 
func takesTooLong() {
    Thread.sleep(forTimeInterval: 5)
    print("Async task completed at \(Date())")
}

Run the app on a device or simulator and click on the “Do Something” button. Output similar to the following should appear in the Xcode console panel:

 

You are reading a sample chapter from SwiftUI Essentials – iOS 16 Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 64 chapters and over 560 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Start 2022-03-29 17:55:16 +0000
Async task completed at 2022-03-29 17:55:21 +0000
End 2022-03-29 17:55:21 +0000

The key point to note in the above timestamps is that the end time is 5 seconds after the start time. This tells us not only that the call to takesTooLong() lasted 5 seconds as expected, but that any code after the call was made within the doSomething() function was not able to execute until after the call returned. During that 5 seconds, the app would appear to the user to be frozen. The answer to this problem is to implement a Swift async/await concurrency structure.

Introducing async/await Concurrency

The foundation of structured concurrency is the async/await pair. The async keyword is used when declaring a function to indicate that it is to be executed asynchronously relative to the thread from which it was called. We need, therefore, to declare both of our example functions as follows (any errors that appear will be addressed later):

func doSomething() async {
    print("Start \(Date())")
    takesTooLong()
    print("End \(Date())")
}
 
func takesTooLong() async {
    Thread.sleep(forTimeInterval: 5)
    print("Async task completed at \(Date())")
}

Marking a function as async achieves several objectives. First, it indicates that the code in the function needs to be executed on a different thread to the one from which it was called. It also notifies the system that the function itself can be suspended during execution to allow the system to run other tasks. As we will see later, these suspend points within an async function are specified using the await keyword.

Another point to note about async functions is that they can generally only be called from within the scope of other async functions though, as we will see later in the chapter, the Task object can be used to provide a bridge between synchronous and asynchronous code. Finally, if an async function calls other async functions, the parent function cannot exit until all child tasks have also completed.

Most importantly, once a function has been declared as being asynchronous, it can only be called using the await keyword. Before looking at the await keyword, we need to understand how to call async functions from synchronous code.

 

You are reading a sample chapter from SwiftUI Essentials – iOS 16 Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 64 chapters and over 560 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Asynchronous Calls from Synchronous Functions

The rules of structured concurrency state that an async function can only be called from within an asynchronous context. If the entry point into your program is a synchronous function, this raises the question of how any async functions can ever get called. The answer is to use the Task object from within the synchronous function to launch the async function. Suppose we have a synchronous function named main() from which we need to call one of our async functions and attempt to do so as follows:

func main() {
    doSomething()
}

The above code will result in the following error notification in the code editor:

'async' call in a function that does not support concurrency

The only options we have are to make main() an async function or to launch the function in an unstructured task. Assuming that declaring main() as an async function is not a viable option in this case, the code will need to be changed as follows:

func main() {
    Task {
        await doSomething()
    }
}

The await Keyword

As we have previously discussed, the await keyword is required when making a call to an async function and can only usually be used within the scope of another async function. Attempting to call an async function without the await keyword will result in the following syntax error:

Expression is 'async' but is not marked with 'await'

To call the takesTooLong() function, therefore, we need to make the following change to the doSomething() function:

 

You are reading a sample chapter from SwiftUI Essentials – iOS 16 Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 64 chapters and over 560 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

func doSomething() async {
    print("Start \(Date())")
    await takesTooLong()
    print("End \(Date())")
}

One more change is now required because we are attempting to call the async doSomething() function from a synchronous context (in this case the action closure of the Button view). To resolve this, we need to use the Task object to launch the doSomething() function:

var body: some View {
    Button(action: {
        Task {
            await doSomething()
        }
    }) {
        Text("Do Something")
    }
}

When tested now, the console output should be similar to the following:

Start 2022-03-30 13:27:42 +0000
Async task completed at 2022-03-30 13:27:47 +0000
End 2022-03-30 13:27:47 +0000

This is where the await keyword can be a little confusing. As you have probably noticed, the doSomething() function still had to wait for the takesTooLong() function to return before continuing, giving the impression that the task was still blocking the thread from which it was called. In fact, the task was performed on a different thread, but the await keyword told the system to wait until it completed. The reason for this is that, as previously mentioned, a parent async function cannot complete until all of its sub-functions have also completed. The call, therefore, has no choice but to wait for the async takesTooLong() function to return before executing the next line of code. In the next section, we will explain how to defer the wait until later in the parent function using the async-let binding expression. Before doing so, however, we need to look at another effect of using the await keyword in this context.

In addition to allowing us to make the async call, the await keyword has also defined a suspend point within the doSomething() function. When this point is reached during execution, it tells the system that the doSomething() function can be temporarily suspended and the thread on which it is running used for other purposes. This allows the system to allocate resources to any higher priority tasks and will eventually return control to the doSomething() function so that execution can continue. By marking suspend points, the doSomething() function is essentially being forced to be a good citizen by allowing the system to briefly allocate processing resources to other tasks. Given the speed of the system, it is unlikely that a suspension will last more than fractions of a second and will not be noticeable to the user while benefiting the overall performance of the app.

Using async-let Bindings

In our example code, we have identified that the default behavior of the await keyword is to wait for the called function to return before resuming execution. A more common requirement, however, is to continue executing code within the calling function while the async function is executing in the background. This can be achieved by deferring the wait until later in the code using an async-let binding. To demonstrate this, we first need to modify our takesTooLong() function to return a result (in this case our task completion timestamp):

 

You are reading a sample chapter from SwiftUI Essentials – iOS 16 Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 64 chapters and over 560 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

func takesTooLong() async -> Date {
    Thread.sleep(forTimeInterval: 5)
    return Date()
}

Next we need to change the call within doSomething() to assign the returned result to a variable using a let expression but also marked with the async keyword:

func doSomething() async {
    print("Start \(Date())")
    async let result = takesTooLong()
    print("End \(Date())")
}

Now all we need to do is specify where within the doSomething() function we want to wait for the result value to be returned. We do this by accessing the result variable using the await keyword. For example:

func doSomething() async {
    print("Start \(Date())")
    async let result = takesTooLong()
    print("After async-let \(Date())") 
    // Additional code to run concurrently with async function goes here
    print ("result = \(await result)")
    print("End \(Date())")
}

When printing the result value, we are using await to let the system know that execution cannot continue until the async takesTooLong() function returns with the result value. At this point, execution will stop until the result is available. Any code between the async-let and the await, however, will execute concurrently with the takesTooLong() function.

Execution of the above code will generate output similar to the following:

Start 2022-03-30 14:18:40 +0000
After async-let 2022-03-30 14:18:40 +0000
result = 2022-03-30 14:18:45 +0000
End 2022-03-30 14:18:45 +0000

Note that the “After async-let” message has a timestamp that is 5 seconds earlier than the “result =” call return stamp confirming that the code was executed while takesTooLong() was also running.

 

You are reading a sample chapter from SwiftUI Essentials – iOS 16 Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 64 chapters and over 560 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Handling Errors

Error handling in structured concurrency makes use of the throw/do/try/catch mechanism previously covered in the chapter entitled “Understanding Error Handling in Swift 5”. The following example modifies our original async takesTooLong() function to accept a sleep duration parameter and to throw an error if the delay is outside of a specific range:

enum DurationError: Error {
    case tooLong
    case tooShort
}
.
.
func takesTooLong(delay: TimeInterval) async throws {
    
    if delay < 5 {
        throw DurationError.tooShort
    } else if delay > 20 {
        throw DurationError.tooLong
    }
    
    Thread.sleep(forTimeInterval: delay)
    print("Async task completed at \(Date())")
}

Now when the function is called, we can use a do/try/catch construct to handle any errors that get thrown:

func doSomething() async {
    print("Start \(Date())")
    do {
        try await takesTooLong(delay: 25)
    } catch DurationError.tooShort {
        print("Error: Duration too short")
    } catch DurationError.tooLong {
        print("Error: Duration too long")
    } catch {
        print("Unknown error")
    }
    print("End \(Date())")
}

When executed, the resulting output will resemble the following:

Start 2022-03-30 19:29:43 +0000
Error: Duration too long
End 2022-03-30 19:29:43 +0000

Understanding Tasks

Any work that executes asynchronously is running within an instance of the Swift Task class. An app can run multiple tasks simultaneously and structures these tasks hierarchically. When launched, the async version of our doSomething() function will run within a Task instance. When the takesTooLong() function is called, the system creates a sub-task within which the function code will execute. In terms of the task hierarchy tree, this sub-task is a child of the doSomething() parent task. Any calls to async functions from within the sub-task will become children of that task, and so on.

This task hierarchy forms the basis on which structured concurrency is built. For example, child tasks inherit attributes such as priority from their parents, and the hierarchy ensures that a parent task does not exit until all descendant tasks have completed.

 

You are reading a sample chapter from SwiftUI Essentials – iOS 16 Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 64 chapters and over 560 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

As we will see later in the chapter, tasks can be grouped to enable the dynamic launching of multiple asynchronous tasks.

Unstructured Concurrency

Individual tasks can be created manually using the Task object, a concept referred to as unstructured concurrency. As we have already seen, a common use for unstructured tasks is to call async functions from within synchronous functions.

Unstructured tasks also provide more flexibility because they can be externally canceled at any time during execution. This is particularly useful if you need to provide the user with a way to cancel a background activity, such as tapping on a button to stop a background download task. This flexibility comes with some extra cost in terms of having to do a little more work to create and manage tasks.

Unstructured tasks are created and launched by making a call to the Task initializer and providing a closure containing the code to be performed. For example:

Task {
    await doSomething()
}

These tasks also inherit the configuration of the parent from which they are called, such as the actor context (a topic we will explore in the chapter entitled An Introduction to Swift Actors), priority, and task local variables. Tasks can also be assigned a new priority when they are created, for example:

 

You are reading a sample chapter from SwiftUI Essentials – iOS 16 Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 64 chapters and over 560 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Task(priority: .high) {
    await doSomething()
}

This provides a hint to the system about how the task should be scheduled relative to other tasks. Available priorities ranked from highest to lowest are as follows:

  • .high / .userInitiated
  • .medium
  • .low / .utility
  • .background

When a task is manually created, it returns a reference to the Task instance. This can be used to cancel the task, or to check whether the task has already been canceled from outside the task scope:

let task = Task(priority: .high) {
    await doSomething()
}
.
.          
if (!task.isCancelled) {
    task.cancel()
}

Detached Tasks

Detached tasks are another form of unstructured concurrency, but differ in that they do not inherit any properties from the calling parent. Detached tasks are created by calling the Task.detached() method as follows:

Task.detached {
    await doSomething()
}

Detached tasks may also be passed a priority value, and checked for cancellation using the same techniques as outlined above:

let detachedTask = Task(priority: .medium) {
    await doSomething()
}
.
.          
if (!detachedTask.isCancelled) {
    detachedTask.cancel()
}

Task Management

Regardless of whether you are using structured or unstructured tasks, the Task class provides a set of static methods and properties that can be used to manage the task from within the task scope.

 

You are reading a sample chapter from SwiftUI Essentials – iOS 16 Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 64 chapters and over 560 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

A task may, for example, use the currentPriority property to identify the priority assigned when it was created:

let detachedTask = Task(priority: .medium) {
    await doSomething()
}
.
.          
if (!detachedTask.isCancelled) {
    detachedTask.cancel()
}

Unfortunately, this is a read-only property so cannot be used to change the priority of the running task. It is also possible for a task to check if it has been canceled by accessing the isCancelled property:

if Task.isCancelled {
    // perform task cleanup
}

Another option for detecting cancellation is to call the checkCancellation() method which will throw a CancellationError error if the task has been canceled:

do {
    try Task.checkCancellation()
} catch {
   // Perform task cleanup
}

A task may cancel itself at any time by calling the cancel() Task method:

Task.cancel()

Finally, if there are locations within the task code where execution could safely be suspended, these can be declared to the system via the yield() method:

 

You are reading a sample chapter from SwiftUI Essentials – iOS 16 Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 64 chapters and over 560 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Task.yield()

Working with Task Groups

So far in this chapter, all of our examples have involved creating one or two tasks (a parent and a child). In each case, we knew in advance of writing the code how many tasks were required. Situations often arise, however, where several tasks need to be created and run concurrently based on dynamic criteria. We might, for example, need to launch a separate task for each item in an array, or within the body of a for loop. Swift addresses this by providing task groups.

Task groups allow a dynamic number of tasks to be created and are implemented using either the withThrowingTaskGroup() or withTaskGroup() functions (depending on whether or not the async functions in the group throw errors). The looping construct to create the tasks is then defined within the corresponding closure, calling the group addTask() function to add each new task.

Modify the two functions as follows to create a task group consisting of five tasks, each running an instance of the takesTooLong() function:

func doSomething() async {
    await withTaskGroup(of: Void.self) { group in
        for i in 1...5 {
            group.addTask {
                let result = await takesTooLong()
                print("Completed Task \(i) = \(result)")
            }
        }
    }
}
 
func takesTooLong() async -> Date {
    Thread.sleep(forTimeInterval: 5)
    return Date()
}

When executed, there will be a 5 second delay while the tasks run before output similar to the following appears:

Completed Task 1 = 2022-03-31 17:36:32 +0000
Completed Task 2 = 2022-03-31 17:36:32 +0000
Completed Task 5 = 2022-03-31 17:36:32 +0000
Completed Task 3 = 2022-03-31 17:36:32 +0000
Completed Task 4 = 2022-03-31 17:36:32 +0000

Note that the tasks all show the same completion timestamp indicating that they executed concurrently. It is also interesting to notice that the tasks did not complete in the order in which they were launched. When working with concurrency, it is important to keep in mind that there is no guarantee that tasks will complete in the order that they were created.

 

You are reading a sample chapter from SwiftUI Essentials – iOS 16 Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 64 chapters and over 560 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

In addition to the addTask() function, several other methods and properties are accessible from within the task group including the following:

  • cancelAll() – Method call to cancel all tasks in the group
  • isCancelled – Boolean property indicating whether the task group has already been canceled. •
  • isEmpty – Boolean property indicating whether any tasks remain within the task group.

Avoiding Data Races

In the above task group example, the group did not store the results of the tasks. In other words, the results did not leave the scope of the task group and were not retained when the tasks ended. As an example, let’s assume that we want to store the task number and result timestamp for each task within a Swift dictionary object (with the task number as the key and the timestamp as the value). When working with synchronous code, we might consider a solution that reads as follows:

func doSomething() async {
 
    var timeStamps: [Int: Date] = [:]
    
    await withTaskGroup(of: Void.self) { group in
        for i in 1...5 {
            group.addTask {
                timeStamps[i] = await takesTooLong()
            }
        }
    }
}

Unfortunately, the above code will report the following error on the line where the result from the takesTooLong() function is added to the dictionary:

Mutation of captured var 'timeStamps' in concurrently-executing code

The problem here is that we have multiple tasks accessing the data concurrently and risk encountering a data race condition. A data race occurs when multiple tasks attempt to access the same data concurrently, and one or more of these tasks is performing a write operation. This generally results in data corruption problems that can be hard to diagnose.

One option is to create an actor in which to store the data. Actors, and how they might be used to solve this particular problem, will be covered in the chapter entitled An Introduction to Swift Actors.

 

You are reading a sample chapter from SwiftUI Essentials – iOS 16 Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 64 chapters and over 560 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Another solution is to adapt our task group to return the task results sequentially and add them to the dictionary. We originally declared the task group as returning no results by passing Void.self as the return type to the withTaskGroup() function as follows:

await withTaskGroup(of: Void.self) { group in
.
.

The first step is to design the task group so that each task returns a tuple containing the task number (Int) and timestamp (Date) as follows. We also need a dictionary in which to store the results:

func doSomething() async {
            
    var timeStamps: [Int: Date] = [:]
 
    await withTaskGroup(of: (Int, Date).self) { group in       
        for i in 1...5 {
            group.addTask {
                return(i, await takesTooLong())
            }
        }
    }
}

Next, we need to declare a second loop to handle the results as they are returned from the group. Because the results are being returned individually from async functions, we cannot simply write a loop to process them all at once. Instead, we need to wait until each result is returned. For this situation, Swift provides the for-await loop.

The for-await Loop

The for-await expression allows us to step through sequences of values that are being returned asynchronously and await the receipt of values as they are returned by concurrent tasks. The only requirement for using forawait is that the sequential data conforms to the AsyncSequence protocol (which should always be the case when working with task groups).

In our example, we need to add a for-await loop within the task group scope, but after the addTask loop as follows:

 

You are reading a sample chapter from SwiftUI Essentials – iOS 16 Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 64 chapters and over 560 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

func doSomething() async {
    
    var timeStamps: [Int: Date] = [:]
    
    await withTaskGroup(of: (Int, Date).self) { group in
        
        for i in 1...5 {
            group.addTask {
                return(i, await takesTooLong())
            }
        }
        
        for await (task, date) in group {
            timeStamps[task] = date
        }
    }
}

As each task returns, the for-await loop will receive the resulting tuple and store it in the timeStamps dictionary. To verify this, we can add some code to print the dictionary entries after the task group exits:

func doSomething() async {
.
.
        for await (task, date) in group {
            timeStamps[task] = date
        }
    }
    
    for (task, date) in timeStamps {
        print("Task = \(task), Date = \(date)")
    }
}

When executed, the output from the completed example should be similar to the following:

Task = 1, Date = 2022-03-31 17:48:20 +0000
Task = 5, Date = 2022-03-31 17:48:20 +0000
Task = 2, Date = 2022-03-31 17:48:20 +0000
Task = 4, Date = 2022-03-31 17:48:20 +0000
Task = 3, Date = 2022-03-31 17:48:20 +0000

Asynchronous Properties

In addition to async functions, Swift also supports async properties within class and struct types. Asynchronous properties are created by explicitly declaring a getter and marking it as async as demonstrated in the following example. Currently, only read-only properties can be asynchronous.

struct MyStruct {
    var myResult: Date {
        get async {
            return await self.getTime()
        }
    }
    func getTime() async -> Date {
        Thread.sleep(forTimeInterval: 5)
        return Date()
    }
}
.
.
func doSomething() async {
 
    let myStruct = MyStruct()
 
    Task {
        let date = await myStruct.myResult
        print(date)
    }
}

Summary

Modern CPUs and operating systems are designed to execute code concurrently allowing multiple tasks to be performed at the same time. This is achieved by running tasks on different threads with the main thread being primarily responsible for rendering the user interface and responding to user events. By default, most code in an app is also executed on the main thread unless specifically configured to run on a different thread. If that code performs tasks that occupy the main thread for too long the app will appear to freeze until the task completes. To avoid this, Swift provides the structured concurrency API. When using structured concurrency, code that would block the main thread is instead placed in an asynchronous function (async properties are also supported) so that it is performed on a separate thread. The calling code can be configured to wait for the async code to complete before continuing using the await keyword, or to continue executing until the result is needed using async-let.

Tasks can be run individually or as groups of multiple tasks. The for-await loop provides a useful way to asynchronously process the results of asynchronous task groups. When working with concurrency, it is important to avoid data races, a problem that can usually be resolved using Swift Actors, a topic we will cover in the next chapter entitled An Introduction to Swift Actors.

 

You are reading a sample chapter from SwiftUI Essentials – iOS 16 Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 64 chapters and over 560 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print