Swift async/await, Structured concurrency, @asyncHandler
In 5 October 2020 Swift team published Swift Concurrency Roadmap. Nowadays we are using basic patterns that protect our data with queues instead of locking. Lock-free operations dispatched on independent queues and returning their results through callbacks. Let’s look at this with an example.
Main motivation in this example completion handlers are suboptimal. Because of this there are some problems. We can list problems like this.
Problem 1: Pyramid of doom
Problem 2: Error handling
Problem 3: Conditional execution is hard and error-prone
Problem 4: Many mistakes are easy to make
Problem 5: Completion handlers are awkward, too many APIs are defined synchronously
Problem 6: Other “resumable” computations are awkward to define
We are not investigating this problem in this article but you can read this article for the proposal of Async/Await.
In Swift Concurrency Roadmap Swift team introduced features in two phases.
The first phase introduces the async syntax and actor types; The second phase will enforce full actor isolation, eliminating data races.
Proposals for the First Phase
- async/await introduces a coroutine-based async/await model to Swift.
- Task API and Structured Concurrency introduces the concept of a task to the standard library.
- Actors & Actor Isolation describes the actor model, which provides state isolation for concurrent programs.
- Concurrency Interoperability with Objective-C introduces automated bridging between Swift’s concurrency features (e.g., async functions) and the convention-based expression of asynchronous functions in Objective-C
- Async handlers introduce the ability to declare a synchronous actor function as an asynchronous handler.
We are focused on async/await, Structured Concurrency, and Async handlers in this article. Before we start we need to making some adjustments to get the project ready
1. Downloading latest Snapshot
2. Install the latest main snapshot of swift.
3. Set Swift Development Snapshot in toolchain. We can achieve this in 2 ways.
Or Xcode -> Preference -> Components > Toolchains
4. Creating a project. We are working on the SwiftPM in this article so let’s create our project.
$ mkdir Concurrency$ cd Concurrency$ swift package init — type executable$ swift build$ swift run
5. Setting the required flag for our project. We have to add -Xfrontend -enable-experimental-concurrency flag to our project.
If you want to try this on a project you should add this flag like this.
We are ready to go.
First things first we need an entry point for calling async function in a non-async function. runAsyncAndBlock is going to our entryPoint for this.
As you can see we importing _Concurrency, underscore at the beginning of Concurrency meaning it is experimental, not suitable for product and the API might be changed in the future.
Let’s create a login scenario.
We have 2 async func and execution does not continue until these functions return a value. Output of this execution is:
But what if we already have functions that have completion like below.
Well there are helper functions that convert your functions that have completion.
We are skipping withCheckedContinuation and withCheckedThrowingContinuation for now but don’t worry i will talk about these 2 functions later. So we can edit our first login function like this.
We can convert the login function to a throwing function using withUnsafeThrowingContinuation.
And ofcourse we should add try? to the beginning of the login function.
What if we want to call async func in a non-async function like this.
Compiler gives an error about this. So we should add @asyncHandler attribute to non-async function
Note: @asyncHandler functions cannot be async function
We are using func sleep(_: UInt32) -> UInt32 method to instead of this we can write our own sleep for good approach.
Replace all sleep(:) function with our new sleep function.
So as i promise now we can talk about withCheckedContinuation and withCheckedThrowingContinuation. Before we do last read again the definition of withUnsafeContinuation and withUnsafeThrowingContinuation.
The operation functions must resume the continuation *exactly once*.
Exactly Once? What if I call multiple times or never call a resume? Well in CheckedContinuation description:
if a continuation is abandoned without resuming the task, then the task will be stuck in the suspended state forever, and conversely, if the same continuation is resumed multiple times, it will put the task in an undefined state.
So CheckedContinuation is a wrapper class for `UnsafeContinuation` that logs misuses of the continuation, logging a message if the continuation is resumed multiple times, or if an object is destroyed without its continuation ever being resumed.
We can use CheckedContinuation instead of UnsafeContinuation for testing purposes. Just replace withUnsafeContinuation to withCheckedContinuation or withUnsafeThrowingContinuation to withCheckedThrowingContinuation.
Thanks for reading.