Introduction of Async, Await and Actors in Swift

Swift

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 asyncwait 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 recognizes 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.

multiply callback function
// 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:

usage of callback
multiply(23) { 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:

nested callback
multiply(23) { 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 often happens in reality. It's 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.

multiply async/await function
// 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 wait. Now we can see that the multiplications are clearer, as they look like they are in a synchronous way.

usage of await
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.

async start in parallel
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', ie 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 increments.

Counterclass
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 increments a thousand times.
We would expect the final number of the value in counter to be 2000 (ie each of the tasks increments 1000 x 2).

Usage of counter
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.

result of data racing
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 defining the Counter as actor instead of class

Actor Counter
actor Counter {
    var value = 0
    func increment() -> Int {
        value = value + 1
        return value
    }
}

and call the increments function with wait, which ensures that each call of the increments will be executed before it starts a new one increments call.

Usage of await
Task.detached {
   print(await counter.increment()) 
}

At the end, we get the expected result = 2000

Prevention of data racing
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, ie 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, feedback, or remarks, I am happy to discuss them with you.

Chin Yiu Chau

Author

Chin Yiu Chau
Application Architect