The Future of Concurrency Handling in Swift | by Pierre-Yves Touzain | Mar, 2022

Source: undraw

The hypo is finally real. Among the introduction of iOS 15 and Xcode 13, Apple brought to the table a new way to support concurrency and write asynchronous and parallel code in a more structured way. It has been proposed by the community on the swift-evolution GitHub repository. If you are curious about it, you can check it out here, it is worth a look. Now let’s get started.

It’s a keyword in Swift short for asynchronous. It’s here to mark that a method will perform an asynchronous task. Here is an example

func getStuff() async -> String {   
return "stuff"
}

For the moment, no real asynchronous task is performed in this method (we will add code here later in this article) but it’s ready to welcome some because we marked the signature of the method with async

It’s another keyword that should be paired with async. When used before to call an async method, it tells the code to wait for its execution and return.

let stuff = await getStuff()
print(stuff)

In this example, print(stuff) won’t be executed until await getStuff() return a value. This is why this method of handling concurrency is so much cleaner than others: it’s simple and efficient as it gets.

Let’s put everything together. If you try to call your async method this way

func fetchData() {
do {
let stuff = try await getStuff()
print(stuff)
} catch {
print(error)
}
}

You will end up with an error from the compiler

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

You can put an async keyword on fetchData() signature to solve the problem…but then you would have to do the same thing for the method calling fetchData() and so on.

So instead, we are going to use aTask.init method to call the asynchronous method from a new task that does support concurrency. This is called unstructured concurrency. This means that the task we are creating here doesn’t have a parent task and so we have complete flexibility to manage it.

And with everything put together, this is the result:

func getStuff() async throws -> String {
let url = URL(string: "https://niceurl/api/stuff")!
let (data,_) = try await URLSession.shared.data(from: url)
let string = String(data: data, encoding: .utf8) ?? ""
return string
}
func fetchData() {
Task.init {
do {
let stuff = try await getStuff()
print(stuff)
} catch {
print(error)
}
}
}

You can note the code added in getStuff(). It is a simple request using URLSession but the interesting thing is that we have another await on the call line. It will obviously wait for the server to answer back before processing the data and returning the string back to the fetchData() method. A small side note also is that you can absolutely add the keyword throws after async and then handle your errors with a try-catch.

Also, this is the kind of code it could replace:

func getStuff(completion: @escaping (Result<String, Error>)->()) {
let url = URL(string: "https://niceurl/api/stuff")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
if let error = error {
completion(.failure(error))
} else if let data = data {
let string = String(data: data, encoding: .utf8) ?? ""
completion(.success(string))
}
}
task.resume()
}
getStuff(completion: { result in
switch result {
case .success(let value):
print(value)
case .failure(let error):
print(error.localizedDescription)
}
})

The async / await code is much clearer and much more readable and so obviously much less error-prone. Also, not using closure avoids the risk of retain cycles (side note: don’t forget to weaken self or any outside properties when you use closures). At last, a nice advantage is that you can use the try-catch on the implementation level as seen before, which is not possible with a completion block.

In order to get everything together, we talked a bit about tasks and more specifically about Task.init. Two other things should be noted. One, is that while Task.init is an unstructured task that runs on the current actor, you also have the possibility to run an unstructured task that’s NOT part of the current actor with Task.detached. And two, both of those methods returns a task handle that allows you some interactions with the task, like its cancellation if needed for example.

func fetchData() {
let taskHandle = Task.detached {
do {
let stuff = try await getStuff()
print(stuff)
} catch {
print(error)
}
}
taskHandle.cancel()
}

If you try to run multiple await calls like this

Task.init {
do {
let stuff0 = try await getStuff(id: 0)
let stuff1 = try await getStuff(id: 1)
let stuff2 = try await getStuff(id: 2)
print(stuff0, stuff1, stuff2)
} catch {
print(error)
}
}

You will face a small problem. Despite that all those getStuff(id:)Calls are asynchronous, only one of them will run at the time, while the others will wait for the previous one to finish before starting running. But in our case, there is no need to wait for the previous call as each of them can run independently.

Thankfully, Apple has a solution for us in order to handle that scenario

Task.init {
do {
async let stuff0 = try getStuff(id: 0)
async let stuff1 = try getStuff(id: 1)
async let stuff2 = try getStuff(id: 2)
try await print(stuff0, stuff1, stuff2)
} catch {
print(error)
}
}

Just write async in front of the let constant you want to assign the result of the call to and then await when you want to use that constant. A small twist in this example is that you need to put try in front of await. And with this implementation, all those getStuff(id:) calls are run separately without waiting for the previous one. Lastly and since we talked about tasks before, the async let syntax create a child task under the hood for you, which can be added and managed in a task group. If you wish to learn more about task groups, check out this article by Paul Hudson.

Similar to classes, it’s a reference type, which means the comparisons between a reference type and a value type, like a struct, apply to the actor type. The key difference is that actors allow only one task to access their mutable state at a time, which makes it safe for code in multiple tasks to interact with the same instance of an actor.

actor Stuff {
let name: String
var numberOf: Int
init(name: String, numberOf: Int) {
self.name = name
self.numberOf = numberOf
}
}

The keyword actor replaces class and you initialize it simply using its initializer.

Stuff(name: "Great Stuff", numberOf: 42)

Then, you pull back the keyword await to access one of the properties of your actor

print(await greatStuff.name)

But there is something important to note here. The code above is a possible suspension point. Because an actor can only have its mutable state accessed by one task at a time, it means that this code won’t execute until the actor allows it to access its property name.

This being said, you do not need to write await to access code from inside the actor. So for example, if you are modifying properties from a method owned by an actoryou are in what’s called the actor local state, which means that only code inside an actor can access this state. Swift guarantees it by making the call of the actor‘s code from outside mandatory to pair with await, failing to compile if not done like it. Apple called it actor and it’s the reason actor are safe.

async / await and actor are about to become the default solution to handle concurrency tasks in Swift. The only thing holding it back for the moment is that it can only be used starting iOS 15. But with the arrival of iOS 16 later this year, this new concept will for sure enrich many codebases in the upcoming months.

Leave a Comment