1. What do we call concurrent programming?
2. Problems of concurrent programming
3. Swift 6 strict concurrency
4. Swift 6 tools for applying strict concurrency
5. How to handle the migration of our projects to Swift 6?
6. Bibliography
At Apple’s Worldwide Developers Conference (WWDC) in June 2024, one of the most anticipated updates for Apple’s development ecosystem was announced: the arrival of Swift 6.
This new version not only brings significant improvements in performance and stability but also introduces a fundamental change in how developers handle concurrency in their applications.
With strict concurrency as one of its main features, Swift 6 marks a milestone in the evolution of the language, providing developers with more powerful and secure tools to manage simultaneous tasks more efficiently.
In this blog, we will explore what this new feature entails, how it impacts application development, and why it represents a crucial step in modernizing the language.
What do we call concurrent programming?
The most basic programming paradigm is structured programming, which uses subroutines or instructions and three essential structures:
- Selective structure: This is the case with conditional structures, such as if-else or switch-case.
- Iterative structure: It is related to subroutines using loops, such as for, while, etc.
- Sequential structure: Instructions are executed in sequence, one after the other, in the strict order in which they were defined.
Concurrent programming, by definition, allows us to handle multiple tasks simultaneously. Although this is not always the case. In reality, a concurrent CPU dedicates a very short period of time to one task and quickly switches to other tasks, so it appears to be executing tasks simultaneously.
Parallel programming is based on two or more CPUs, or in our case, several cores, which can execute several tasks simultaneously.
However, Swift is not capable of deciding which parts of code can be divided for parallel execution. It is the developer’s job to specify which tasks can be divided and how the application should continue executing once they have completed.
The devices on which our applications run are becoming increasingly more powerful. For example, the iPhone 16 has a dedicated processor, the A18, with a 16-core Neural Engine. If our code is not adapted to parallel programming, we will be using only a fraction of the performance the device offers.
Furthermore, applying parallel programming will not only make our application perform faster, but will also ensure it always runs smoothly, without crashes or unwanted execution delays.
Problems of concurrent programming
The biggest problem we face when working with concurrent programming is shared access to the same data from multiple simultaneous tasks.
This is known as data race and refers to what happens when two tasks access a piece of data simultaneously. Imagine a warehouse with 10 products. We launch two simultaneous tasks to check that 10 products are available and sell them all to a different customer in each task. If the available products are read simultaneously, both tasks will see that 10 are in stock and 20 will be sold, even though only 10 are available. This inconsistency in data reading in our applications can lead to inconsistent data, unexpected behavior, or even crashes during execution.
Another problem that can arise is a deadlock, which occurs when one thread waits for a second thread to finish, while the second thread waits for the first thread to finish, blocking each other.
Starvation, on the other hand, occurs when the execution of a thread is constantly postponed, waiting for other higher-priority threads to finish, and never completes.
Furthermore, we must not overlook the fact that working with concurrency in our code adds an extra layer of complexity that can be difficult to maintain and scale. Debugging concurrent code is also more complex, as it can produce errors that are not easily identifiable. Finally, poor concurrent programming can lead to non-optimal use of memory or CPU resources and hamper our application’s performance.
Strict concurrency in Swift 6
To help developers’ code adapt to the increasing performance of Apple’s devices, the latest version of the language, Swift 6, adopts the strict concurrency paradigm imperatively.
Strict concurrency consists of the compiler automatically searching for parts of the code where race conditions or data races may occur. In other words, Swift 6 ensures that the code is safe from data races.
Since the migration can be quite complex and time-consuming, Apple offers the option of gradually activating concurrency checks starting with Swift 5.5, separated into three independent blocks:
- DisableOutwardActorInference: This means eliminating the actor isolation inference caused by property wrappers. Until Swift 6, property wrappers that have their wrapped value marked with @MainActor are inferred to also be @MainActor. For example, using a @StateObject in a SwiftUI View automatically makes the entire View @MainActor. Enabling this check in Swift 5.5 stops property wrappers from applying this behavior.
- GlobalConcurrency: Applies strict concurrency to global variables. The problem with these types of variables is that they can be accessed and modified from any context, making them susceptible to race conditions. After activating this check, it is advisable to isolate the variable to a global actor or make it immutable and Sendable.
- InferSendableFromCaptures: There are some residual assumptions of functions and literal access paths that may collide with the concurrency application.
- Partially applied functions: They are typical of functional programming and are defined by taking multiple arguments and transforming them into another function with fewer arguments. The problem arises when these types of functions capture variables that can be accessed from multiple contexts, which can lead to race conditions. The proposal to adapt them to concurrency is to mark them as Sendable types.
- Key path literals: They can capture any value that is not of type Sendable, potentially causing concurrency issues. Therefore, enabling the check will generate warnings indicating that captured values must be of type Sendable.
If we run the checks from Swift 5.5, they will be displayed as warnings and not as compilation errors, allowing us to continue developing without having to be blocked while we resolve all the detected concurrency issues.
Swift 6 tools to ease strict concurrency adoption
- Asynchronous functions: These are functions that can be paused during execution. A simple example would be those functions that make requests to the backend to receive a response. They must be marked with async to indicate that execution must be halted until the method returns what is expected of it. When we invoke it, we always mark it with await, since the suspension of execution is never implicit. By doing this, the thread in which it is executing is freed up to continue with other tasks, until it returns. At that point, execution will resume in the same thread or another. It is important to use async-await in our asynchronous functions since there is no safe way to call them from synchronous code. Even if we use try-catch blocks to capture possible errors or Result to encapsulate them, we will not be complying with strict concurrency.
- Tasks and Task Groups: A task is a unit of work that can be executed asynchronously. A task itself is sequential, but if we have multiple tasks, they can be executed simultaneously. Tasks can belong to a task group, which gives us the advantage of being able to dynamically add tasks, sort them by priority, or cancel them. They are arranged hierarchically. Additionally, each task can have associated child tasks. Using tasks in task groups is what is called structured concurrency. Swift also allows us to use them independently, giving us the flexibility to equally apply unstructured concurrency.
- Actors: Unlike Tasks, which are isolated units of code, Actors allow us to share information between concurrent code by allowing access to their mutable states to only one task at a time. This prevents race conditions. To do this, when you want to access a property or method of an actor, you must mark it with await (temporary suspension of code execution), which will cause it to wait if those properties or methods are being accessed elsewhere in the code at that time.
(Example of the tools explained above)
- Asynchronous sequences: In these cases, where several asynchronous functions depend on the completion of previous functions, it is advisable to use a for-await-in loop. In this case, the loop will pause execution at each iteration until it completes and then continue with the next one.
- Sendable type: A concurrency domain is the part of an actor or task that contains mutable states, such as variables and properties. Some data cannot be shared across concurrency domains because overlapping access to that data is not protected. For this type of data to be shareable, it must implement the @Sendable protocol. This requires that its variables and properties be immutable, or, if they are mutable, we must ensure that they are thread-safely accessible.
How to approach the migration of our projects to Swift 6?
In order to guarantee a successful migration it is advisable to approach the migration from Swift 5.5 by enabling concurrency checking in the compiler gradually.
It is also important to thoroughly understand the concepts of strict concurrency and the various assumptions we will be facing. We must also be familiar with the code we are migrating, knowing where we are starting from and where we want to go. Therefore, we should not approach it hastily. If we do, the resulting code may be less efficient, readable, and scalable.
If the code is modularized, it is a good idea to start with a small module and migrate gradually.
Bibliography
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/
https://developer.apple.com/documentation/swift/adoptingswift6
https://www.hackingwithswift.com/quick-start/concurrency/concurrency-vs-parallelism
https://www.massicotte.org/concurrency-swift-6-se-401
https://paul-samuels.com/blog/2018/01/31/swift-partially-applied-functions/