If you are eager to test the new async
/await
Swift feature, you are lucky. Three new Swift evolution proposals have been drafted, the first one submitted and in review. These proposals have been partially implemented (available in the latest snapshots from main
branch).
- Proposal SE-0296: Async/await
- Proposal SE-NNNN: Structured concurrency
- Proposal SE-NNNN: Concurrency Interoperability with Objective-C
Before you get started, please consider this:
NOTE: The implementation on these proposals is not final, and it might change while the proposals are being reviewed. Furthermore, these implementations are buildig blocks, which on their own do not provide enough funtionality for fully taking advantage of
async
/await
.
Here are the steps to get you set up.
Install the latest Swift Snapshot
If you are on a Mac, you can download the latest Swift snapshot (Dec 5) for Xode from this url:
Other platforms are available on the downloads page.
Once downloaded, install the package and proceed to the next step to configure Xcode.
Configure Xcode
In Xcode, open Xcode -> Prefences
and navigate to Components -> Toolchains
. From the list, select the Swift toolchain just installed (Dec 5)
Package Setup
For me, the easiest way to test new stuff is from the command line (I find executables super easy to set up).
In the command line, make a new folder (any name you want) and cd
into it:
mkdir async-await-cli-demo
cd async-await-cli-demo
Create a new Swift executable:
swift package init --type executable
Open the package in Xcode:
open Package.swift
Edit Package.swift
to add the Swift compiler flags -Xfrontend -enable-experimental-concurrency
. Your package manifest should look something like this:
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "async-await-cli-demo",
dependencies: [
],
targets: [
.target(
name: "async-await-cli-demo",
dependencies: [],
swiftSettings: [
.unsafeFlags([
"-Xfrontend",
"-enable-experimental-concurrency"
])
]
),
.testTarget(
name: "async-await-cli-demoTests",
dependencies: ["async-await-cli-demo"]
),
]
)
Write some code
In Xcode, open main.swift
and proceed to write our first async function!
func asyncMethod(name: String) async {
for x in 1...3 {
print("Async hello, \(name)! \(x)")
sleep(1)
}
}
This method will print three messages to the console, sequentially, one second apart.
Turns out, we cannot call this method from main.swift
, not even within a DispatchQueue.main.async
block. As stated in the proposal, this in intentional:
Because only async code can call other async code, this proposal provides no way to initiate asynchronous code. This is intentional: all asynchronous code runs within the context of a “task”
The idea behind this is that synchronous code (e.g. main thread of an application) will need to define asynchronous entry points, something covered in the Structured Currency proposal.
I was bummed for a little bit, because I really wanted to test this code out. I played a bit with DispatchQueue
and Task
, but decided to reach out for help. Thanks to Doug Gregor’s quick response on Twitter, I was able to borrow his test extensions and move past this point.
extension DispatchQueue {
func async<R>(execute: @escaping () async throws -> R) -> Task.Handle<R> {
let handle = Task.runDetached(operation: execute)
// Run the task
_ = {
self.async {
handle.run() // deprecated method, for testing purposes only
}
}()
return handle
}
@discardableResult
func async<R>(in group: DispatchGroup,
execute: @escaping () async throws -> R) -> Task.Handle<R> {
let handle = Task.runDetached(operation: execute)
// Run the task
group.enter()
_ = {
self.async {
handle.run() // deprecated method, for testing purposes only
group.leave()
}
}()
return handle
}
}
The above two extensions allow launching an async task from a DispatchQueue
, with or without a DispatchGroup
. Note the run()
method is flagged as deprecated, and it is intended for testing purposes only.
Calling asynchronous code
With the above extensions, we can write code like this:
let group = DispatchGroup()
DispatchQueue.global().async(in: group) {
await asyncMethod(name: "single")
}
group.wait()
This code will print the following:
Async hello, single! 1
Async hello, single! 2
Async hello, single! 3
Not bad, our first use of async
/await
. Non-blocking code without callback closures 🎉
Sequential calls
Let’s try sequential calls:
DispatchQueue.global().async(in: group) {
await asyncMethod(name: "first")
await asyncMethod(name: "second")
}
group.wait()
This code will execute the two async methods sequentially, one after another, and print:
Async hello, first! 1
Async hello, first! 2
Async hello, first! 3
Async hello, second! 1
Async hello, second! 2
Async hello, second! 3
No more piramid of doom, promises or futures needed!
Note that using await
, we can use the result of the first async operation as a parameter for the second one:
func sum(_ a: Int, _ b: Int) async -> Int {
return a + b
}
DispatchQueue.global().async(in: group) {
let c = await sum(1, 2)
let d = await sum(c, 3)
print(d) // 6
}
group.wait()
Parallel/concurrent calls
According to the documentation for Child Tasks, the following code should work:
DispatchQueue.global().async(in: group) {
async let foo = sum(1,2) // error: Call is 'async' in an 'async let' initializer that is not marked with 'await'
async let bar = sum(3,4) // error: Call is 'async' in an 'async let' initializer that is not marked with 'await'
print(await foo + bar)
}
group.wait()
async let
assignments look very interesting and powerful. Seems like a great way to structure concurrent code.
Meanwhile, we can achieve parallelism as we are used to, by using separate group tasks:
var foo: Int = 0
DispatchQueue.global().async(in: group) {
foo = await sum(1,2)
}
var bar: Int = 0
DispatchQueue.global().async(in: group) {
bar = await sum(3,4)
}
group.wait()
print(foo + bar)
While this code works, it is prone to errors, including thread-safety. I’m hoping for the async let
implementation to be finished soon.
My thoughts
I have no much to say, actually, other than async
/await
is awesome and cannot be more grateful for all the people that have worked hard for making this happen.
I firmly believe this will be one of the most important additions to the Swift language, if not the most. Chances are this will have great impact in how we write code in the future. And this is for the better of the entire Swift community.