async/await in Server-Side Swift and Vapor
Learn how Swift’s new async/await functionality can be used to make your existing EventLoopFuture-based Vapor 4 code more concise and readable. By Mahdi Bahrami.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
async/await in Server-Side Swift and Vapor
25 mins
- Getting Started
- Bridging Between async/await and EventLoopFuture
- Converting EventLoopFuture to async/await
- Converting async/await to EventLoopFuture
- Understanding Asynchronous Tasks in Synchronous Contexts
- Leveraging async/await in Vapor
- Becoming More Fluent Than Ever!
- Updating Fluent Migrations
- Understanding Concurrent Loading
- Understanding Concurrent Loading with TaskGroup
- Understanding Concurrent Loading with async let
- Where to Go From Here?
Understanding Concurrent Loading with TaskGroup
Look at trade(req:)
again, and recheck how these two codes compare to each other.
This is your old EventLoopFuture
code:
allTrades.map { tradingSides in
tradeOne(on: req.db, tradingSides: tradingSides)
}
.flatten(on: req.eventLoop)
And this is your new async/await code:
for tradingSides in allTrades {
_ = try await tradeOne(on: req.db, tradingSides: tradingSides)
}
The problem is that all trades in the EventLoopFuture
version of your code are running simultaneously. However, in the async/await version, Swift executes them one after another. With EventLoopFuture
, every task starts possibly as soon as an EventLoopFuture
is created. But with the async/await version of this code, every time Swift hits an await
in the loop, it’ll stop and wait for the result of the task. This makes your code’s async/await variant slower when multiple trades happen.
The solution is to use a TaskGroup
. TaskGroup
is a group of tasks that are done concurrently. To get access to a TaskGroup
, you need to use either withTaskGroup(of:returning:body:)
or withThrowingTaskGroup(of:returning:body:)
. The difference between those two functions is that the second function gives you access to a ThrowingTaskGroup
whose addTask(priority:operation:)
accepts throwing operations. ThrowingTaskGroup
, as its name suggests, is just a throw-friendly version of a TaskGroup
.
A better conversion for trade(req:)
is:
private func trade(req: Request) async throws -> HTTPStatus {
// 1
let allTrades = try req.content.decode([TradeItem].self)
// 2
return try await withThrowingTaskGroup(
of: HTTPStatus.self
) { taskGroup in
// 3
for tradingSides in allTrades {
taskGroup.addTask {
try await tradeOne(on: req.db, tradingSides: tradingSides)
}
}
// 4
try await taskGroup.waitForAll()
// 5
return .ok
}
}
Here, you:
- Decode an array of
TradeItem
s from the body of the request sent to you. - Use
withThrowingTaskGroup(of:returning:body:)
to make a newTaskGroup
. Theof
argument defines the type of value that you’ll return when using theaddTask(priority:operation:)
function. Note that you see noreturning
argument because Swift can automatically infer that thisTaskGroup
will return anHTTPStatus
. - Iterate through the tasks and use the
addTask(priority:operation:)
to add a new task to theTaskGroup
for eachtradingSides
. For thepriority
argument, you can use the default value. - Wait for all tasks to finish. An important note is that even without
waitForAll()
, Swift returns from the closure only when all tasks are done. Use ofwaitForAll()
here is to make sure all thrown errors are caught. Without it, even if onetradeOne(on:tradingSides:)
throws an error, you wouldn’t be notified of it. - Return a
200 OK
HTTP status.
Note that you can iterate through each value in a TaskGroup
and capture results of each task. You can either use the next()
function on a TaskGroup
, or use a loop like so:
for await taskResult in taskGroup {
// do something with the `taskResult`
}
Luckily, you don’t need the results of the trades here, so you don’t need to capture any of the results.
Understanding Concurrent Loading with async let
Now that you’ve fixed the trade(req:)
function, look at tradeOne(on:tradingSides:)
. It contains these two lines:
try await crate1.save(on: db)
try await crate2.save(on: db)
That’s another piece of suboptimal code! Trader waits once for each crate’s save operation, even though the operations aren’t dependent on each other. To solve that, you can still use a TaskGroup
, but that would be overkill. The better way is replacing those two lines with the code below:
async let crate1Saving: Void = crate1.save(on: db)
async let crate2Saving: Void = crate2.save(on: db)
_ = try await (crate1Saving, crate2Saving)
You should’ve noticed that although save(on:)
performs an async
work, there’s no await
keyword behind it when assigning it to an async let
. async
variables are a new addition to Swift. They simply allow assigning an async
function’s value to a variable without actually awaiting the asynchronous operation at that point. This is the exact thing happening in the code above. You’re assigning two save operations’ values to crate1Saving
and crate2Saving
, but you’re postponing the await
process to be done somewhere else at another time.
The last line is what awaits on both processes to complete before continuing. There, you assign the save processes to a tuple by declaring (crate1Saving, crate2Saving)
. Then, you use try await
to await both of them simultaneously. The _ =
part of the code is there because you don’t need the results of the operations.
For your use cases, TaskGroup
and async let
were the best solutions, but you can also use the Task.detached(priority:operation:)
function to run tasks concurrently. Learning about that will have to wait for another time. :]
Where to Go From Here?
You can download the sample project by clicking Download Materials at the top or bottom of this tutorial.
In this tutorial, you learned about the most important Vapor-related concurrency features, but there’s still a lot left to learn.
To learn about all the new concurrency features, check out the book Modern Concurrency in Swift.
For more about Vapor’s new async/await APIs, see the official Vapor documentation.
We hope you enjoyed this tutorial, and if you have any questions or comments, please join the forum discussion below!