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.

5 (4) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

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:

  1. Decode an array of TradeItems from the body of the request sent to you.
  2. Use withThrowingTaskGroup(of:returning:body:) to make a new TaskGroup. The of argument defines the type of value that you’ll return when using the addTask(priority:operation:) function. Note that you see no returning argument because Swift can automatically infer that this TaskGroup will return an HTTPStatus.
  3. Iterate through the tasks and use the addTask(priority:operation:) to add a new task to the TaskGroup for each tradingSides. For the priority argument, you can use the default value.
  4. 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 of waitForAll() here is to make sure all thrown errors are caught. Without it, even if one tradeOne(on:tradingSides:) throws an error, you wouldn’t be notified of it.
  5. 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!