
Mai 2022
Introduction of Async, Await and Actors in Swift
Since the release of Xcode 13, Swift introduced several new keywords. The most anticipated ones could be async, await and actor. In this blog, I would like to talk about these new features in Swift and discuss the reasons why and how these new features would benefit us so much in our implementation of asynchronous.
Async/Await
It is a very normal practice that we start some operations or tasks concurrently. For example, network calls, database read/write etc. These tasks might take longer time; however, you don’t want them to block your whole app operation. Therefore, concurrency is one of the basic requirements of an app.
In order to take on some tasks in an asynchronous way and tell if these have been completed, we have to use callbacks beforehand.
For example we have a multiply function which recognises input_a and input_b as integers. Using callback to deliver the product (return value) of the multiplication, we can add a two seconds delay to simulate network latency.
// implementation of multiply function with callbacks func multiply(_ input_a: Int, _ input_b: Int, completion: @escaping (Int) -> Void){ DispatchQueue.main.asyncAfter(deadline: .now() + 2.0 ){ completion(input_a * input_b) } } |
For call this function:
multiply( 2 , 3 ) { result in // your implementation print(result) //expected result = 6 } |
Everything looks fine up to this point. However, in the reality, there are a lot of complicated operations taking place during an asynchronous process.
This kind of processes might also initialize others:
For example by using the provided multiply function, we want to calculate 2 x 3 x 4 x 5.
This is one of the cases, in which a nesting asynchronous looping could happen.
The implementation would look like this:
multiply( 2 , 3 ) { result in multiply (result, 4 ) { result2 in multiply (result2, 5 ) { result3 in print(result3) // expected result = 120 } } } |
As you can see, the result is a nested callback chunk, which happens often in reality. It is not that easy to maintain this piece of code. Let’s have a look at Swift 5.5 and how this gets handled with async/await.
First, we have to amend the function multiply by adding the keyword async as a method attribute to declare that this function performs asynchronously.
// implementation of multiply function with callbacks func multiply(_ input_a: Int, _ input_b: Int) async throws -> Int{ try await Task.sleep(nanoseconds: UInt64( 2 * Double(NSEC_PER_SEC))) //some async delay return input_a * input_b } |
Then we are ready to use await. Now we can see that the multiplications are clearer, as they look like they are in a synchronous way.
Task { let result = try await multiply( 2 , 3 ) let result2 = try await multiply( 4 , result) let finalResult = try await multiply(result2, 5 ) print(finalResult) // expected result = 120 } |
If your asynchronous processes do not depend on each other, they can also start simultaneously.
The following code will start the multiply functions simultaneously by simply putting async in front of the return value that will wait until the three multiplication results are delivered.
Task { async let result1 = multiply( 2 , 3 ) async let result2 = multiply( 6 , 7 ) async let result3 = multiply( 4 , 5 ) let response = try await [result1, result2, result3] print(response) } |
Await allows you to delay the execution of a function until the other execution is finished. Await also allows the system to complete other tasks while the async tasks are being executed.
Actors
One of the main purposes of actor is preventing so-called ‘data races’, i.e. memory corruption issues that can occur when two separate threads try to access or mutate the same data simultaneously. For example, we have a counter class with a variable value and a function called increment.
class Counter { var value = 0 func increment() -> Int { value = value + 1 return value } } |
Let’s say there are two tasks. Task A and Task B started in different threads and access the same instance counter.
Then both Task A and Task B call the function increment a thousand times.
We would expect the final number of the value in counter to be 2000 (i.e. each of the tasks increment 1000 x 2).
let counter = Counter() //Task A Task.detached { for _ in 1 ... 1000 { print( "In Task A: \(counter.increment())" ) } } //Task B Task.detached { for _ in 1 ... 1000 { print( "In Task B: \(counter.increment())" ) } } sleep( 20 ) //waiting for 20 seconds print( "total: \(counter.value)" ) |
However, if there is no data races prevention, the final value of the counter value will be unknown and almost certainly not 2000.
In Task B: 1977 In Task A: 1979 In Task B: 1979 In Task A: 1981 In Task B: 1981 In Task A: 1983 In Task B: 1984 In Task A: 1984 In Task B: 1986 In Task B: 1986 In Task B: 1987 In Task B: 1988 In Task A: 1989 In Task A: 1990 In Task A: 1991 In Task A: 1992 In Task A: 1993 total: 1993 |
So the question is: How can we make sure that the counter will increment the value one by one? In this case, actor can easily fix this problem by simply define the Counter as actor instead of class.
actor Counter { var value = 0 func increment() -> Int { value = value + 1 return value } } |
and call the increment function with await, which ensures that each call of the increment will be executed before it starts a new increment call.
Task.detached { print(await counter.increment()) } |
At the end, we get the expected result = 2000
In Task B: 1988 In Task A: 1989 In Task B: 1990 In Task A: 1991 In Task B: 1992 In Task A: 1993 In Task A: 1994 In Task A: 1995 In Task A: 1996 In Task A: 1997 In Task A: 1998 In Task A: 1999 In Task A: 2000 total: 2000 |
Conclusion
Async/Await changes the way to maintain the nested callbacks of asynchronous operations. The new feature in Swift 5.5 improves the code readability and maintainability. Actor, on the other hand, simplifies the implementation of data race prevention mechanism.
One of the great things about the new concurrency system is backward compatibility, i.e. it supports all the way back to iOS 13, macOS Catalina, watchOS6, and tvOS 13.
I would highly recommend implementing such new features in your app. It will help to significantly ease the implementation of complicated and nested callbacks.
Here is the demo code. Feel free to play around.
https://github.com/chauchinyiu/ConcurrencyDemo
Should you have any further questions, feedbacks, or remarks, I am happy to discuss them with you.

Author
Chin Yiu Chau
Application Architect