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
TradeItems from the body of the request sent to you. - Use
withThrowingTaskGroup(of:returning:body:)to make a newTaskGroup. Theofargument defines the type of value that you’ll return when using theaddTask(priority:operation:)function. Note that you see noreturningargument because Swift can automatically infer that thisTaskGroupwill return anHTTPStatus. - Iterate through the tasks and use the
addTask(priority:operation:)to add a new task to theTaskGroupfor eachtradingSides. For thepriorityargument, 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 OKHTTP 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!