Async/ Await (Modern Concurrency in Swift)
Async/ Await in Swift (Modern Concurrency)
Async/ Await
What is async/Await?
Apple introduces a new concurrency feature i.e., asynchronous (async) from Swift 5.5.
Async:- stands for asynchronous, which means to execute the methods/program asynchronously.
Await:- is the keyword to be used for calling async methods. Await is awaiting the result. It indicates that your program might pause its execution while it waits for the result.
Platform Supported :
- Swift Version:- Swift 5.5
- iOS Version: – iOS 15 onwards
- Backward compatibility: – iOS 13, Xcode 13.2, macOS 10.15, watchOS 6, and tvOS 13. This backward compatibility applies only to Swift language features; you can not use any APIs built using those language features, like the new URLSession APIs that use async/await(for that, you still require iOS 15.0).
How to create and call an asynchronous function?
The below function fetched the thumbnail and returned UIImage and String in a tuple. In this new type of asynchronous (async) function, we just add an async keyword before the return type. After that create a URL request, fetch the data from that request and create a thumbnail.
Here is the extension of UIImage to generate a thumbnail. byPreparingThumbnail is an asynchronous method which is why it marks as await.
This method is marked as an async throw which means it executes asynchronously and throws an error, if any.
When calling an asynchronous function, We have to use await in front of the call to mark possible suspension points. When we request to download an image, asynchronous code suspends and frees the thread and handover that thread to the system to perform some other important/Concurrent Task. And when it resumes later, it executes further on that same thread.
Call async function serially :
In the above code, all images are downloaded serially. We tell applications that wait for the first image to be returned until it can continue to fetch a second image, and so on. All images are in download in sequence. After that, We create an array of all downloaded images.
Async let: call async function parallel.
When we want to download images parallel, they have no dependent operation. We just need to put an async keyword before the let. Our array of images now needs to be defined using the await keyword as we’re dealing with asynchronous constants.
Drawback of completion handler approach:
- It’s possible for the functions to call their completion handler more than once, or might possibly forget to call it entirely.
- In the above code, there are five possibilities for subtle bugs if we are unaware or forgot the completion handler
- The parameter syntax @escaping (String) -> Void can be hard to read.
- It was hard to send back errors with the completion handler (Until swift 5.0 added the Result type Result<value>)
- Thread Explosion: The completion handler approach blocks the thread and frees the thread when the function returns the result. So when the system creates multiple threads, and then the system is overcommitted, thread explosion occurs.
Benefit of the async await approach :
- Avoid the Pyramid of Doom problem with nested closures
- Reduction of code
- Easier to read
- Safety. With async/await, a result is guaranteed, while completion blocks might or might not be called.
- It doesn’t block the thread. Async suspended the function call and freed up the thread for other important work (that is decided by the system). The completion handler approach blocks the thread and frees the thread when the function returns the result
- No thread explosion
- Co-Operative thread pool: – Avoid thread explosion and excessive context switching. It is designed in such a way that it reuses the ideal thread instead of blocking them.
Here are a few important things to remember about async/await.
- First, when you mark a function async, you’re allowing it to suspend. And when a function suspends itself, it suspends its callers too. So its callers must be async as well.
- await means async function may suspend execution one or many time
- when an async function is used thread is not blocked
TASK
Task: Task is a Unit of asynchronous work. Every async function/method is executed in a Task.
How to create and run a Task ?
Performing async method inside a Task
As you see above, when we call let firstImage = try? await fetchThumbnail(for: “23”) we got an error from the compiler I.e. “‘async’ call in a function that does not support concurrency ”
What does that mean?
From WWDC21 The Swift compiler tells us that we cannot call async functions in contexts that aren’t themselves async. when you’ve tried to call an async function from a synchronous environment, which is not allowed. Asynchronous functions must be able to suspend themselves and their callers, and synchronous functions don’t know how to do that.
So what is the solution for this error? The solution is a Task!
The async task packages the work in the closure and sends it to the system for immediate execution on the next available thread, like an async function on a global dispatch queue.
Another way to solve this error is, We can mark the function async but using this; we will get an error in the upper hierarchy
Like somewhere when we call fetchThumbnail() method, it gives the same error “‘async’ call in a function that does not support concurrency.” So to resolve this again we have to use the task
Task has more feature like:
1. Task priority: We can decide task priority like background, utility etc, but priority can also be nil if no priority is assigned as we have done above.
2. Cancel: – Canceling task and its child task : canceling a parent task automatically cancels all of its child tasks.
3.Task.currentPriority: to check task priority
Task Group
TaskGroup: TASK GROUP Help us to execute the concurrent and parallel tasks and group its result to return as the final (single) output
Creating a task: The task comes in two flavors.
- By throwing an error
2. Without throwing an error:
To create a task group, we can use withTaskGroup or withThrowingTaskGroup according to our needs. For now, we are going ahead withTaskGroup. So our code looks like below:
In this example, we will create a task group that has multiple child tasks that execute a UserOperation and return its result. When all operations end, the task group will collect all child task results and return them to a dictionary.
Let’s say we have an array of Users, as shown below:
We are returning a couple for the child task, and the task group returns a dictionary that contains the user’s full name and initials.
As you can see, we have looped all the operations and added the child task to the group to process full name and initial. All child tasks run concurrently, and there is no control over when they finish, to collect the result of all child tasks, we have a loop task group. As we see above, the await keyword in looping indicates that the for-loop might suspend while waiting for a child task to complete. Whenever the child task returns the result, the for-loop iterates and updates the dictionary, and when all child is complete, then the for-in group exits and returns the group result.
When we run this, we got the following result :
As you see, the result operation is completed in 10 sec. And all child tasks run concurrently and the task group returns only when all child tasks are completed. This also indicates that the child is only available in a task group context.
Actor
The Actor is used to protect the mutable state in swift. It helps us to protect data races.
Data Races occur when:
- Two threads concurrently access the same data
- And any of them modify the same data
Actor are similar to the class in most cases. Some points are discuss below:
- Actor are reference type
- Actor have properties, methods, initializers, deinitializer, and subscripts
- Do not support inheritance.
- Can execute one method at a time
- It automatically confirms to Actor protocol.
Here is some old thing that can replace by Actor :
- DispatchQueue.main.async use MainActor
- DispatchBarrier/Lock use Actor
- Data Race use Actor
Let’s jump to the old, How can we avoid data races? We used Dispatch Barrier or lock to save our program from DataRaces.
Here we create a small bank module in which we can deposit, withdraw and check the bank balance. In this example, we create a barrier queue to prevent data races while accessing mutable availableBalance variables. In all functions, we have used a barrier flag to allow access to one thread at a time.
Now we have created a queue to asynchronously withdraw and deposit amounts in our bank.
As we run this, we can see the initial balance in the bank is 100, and when total balance after withdrawal and deposit is 90. This looks perfectly fine because this is an asynchronous operation. It’s possible to withdrawal before the deposit and vice versa.
Now let’s do this again using Actor. We have changed Class to Actor here and removed the barrier queue, which is used in class examples. An actor has serialized all its properties, ensuring that only one interaction happens at a single time, giving us complete protection against data races because all mutations are performed serially. So in the Bank actor, we have a mutable available balance, and it accesses serially; hence it protects against data races.
To support concurrency, we just mask await and bind this into the task, as explained in the async await and task section.
@MainActor: The main actor is a globally unique actor which performs tasks on the main thread.
Earlier anything we need to update the UI in the main thread we have to use dispatchQueue.main.async, but using the @mainActor, we don’t require this to update the UI in the main thread. In our bank example, there is a label on the UI on which we are going to show the balance. So to update UI on the main thread we have to mark showBalance function with @mainActor attribute.
Main actor is a global actor that we can use with properties, methods, closures, and instances. For example, we have added an attribute @mainActor before the function declaration meaning that the function will be executed in the main thread.
Conclusion: That’s all for now. Thanks for reading. In Modern concurrency, there are more concepts remaining, like structure concurrency, actor isolation, actor hopping, etc but it’s not possible to cover all these sections, you can also check this out in WWDC21 and WWDC22.