async/await在swift服务器vapor中的使用指南【译】
版本:Swift5.5,macOS 12,Xcode 13
过去几年,在Swift服务器端Vapor中,都使用EventLoopFuture
执行异步任务。尽管EventLoopFuture
功能强大,但是它依然有一些问题。首先,它使用闭包,这会慢慢使我们不能书写干净又可读的代码。第二,它需要花时间学习。async/await可以很好解决这两个问题。
在使用async/await后,你的代码会看起来像同步代码一样,没有任何闭包,并且更加易于阅读。很多人已经在vapor中使用它们了,并且目前为止,反响非常好。
在这个教程,你将会把一个使用EventLoopFuture
的项目变为使用async/await。这个项目包含以下内容:
- 实现交易API
- 使用Fluent在数据库储存信息
- 将routes迁移到async/await
译者注:routes是vapor中包含各种路由的地方。
小贴士:完全使用async/await需要Swift 5.5, Xcode 13.1 和 macOS Monterey 以上的操作系统
学习这个教程的前提是你已经熟练编写Vapor4程序。如果你是新手,请查看Getting Started with Server-Side Swift with Vapor 4
同时你将配合PostgreSQL与Fluent一起使用。如果你对Fluent和在Docker运行数据库不熟悉,请查看Using Fluent and Persisting Models in Vapor
起步
下载示例项目。这是一个简单的项目,只有一个API跟踪不同用户之间的交易。你会将它转变为使用async/await。
译者注:请前往https://www.raywenderlich.com/29095914-async-await-in-server-side-swift-and-vapor下载示例项目
打开项目,你可以发现里面包含了大量的文件和文件夹。

第一步,打开Package.swift
,因为你将要使用async/await,所以你需要做一些修改。
在文件的第一行,定义了该程序使用的swift版本,请确保它大于5.5
// swift-tools-version:5.5
在下方的Package
定义中,改变macOS版本到12
platforms: [
.macOS(.v12)
],
最后,在targets
中,定义一个executableTarget
的运行目标,而不是普通target
.executableTarget(name: "Run", dependencies: [.target(name: "App")]),
在Swift5.5中,可运行目标是必须的。在Xcode解决项目依赖关系的时候,打开终端。拷贝并粘贴以下代码到终端中,让你的PostgreSQL数据库在Docker中运行起来。
docker run --name traderdb -e POSTGRES_DB=vapor_database \
-e POSTGRES_USER=vapor_username \
-e POSTGRES_PASSWORD=vapor_password \
-p 5432:5432 -d postgres
这将会创建一个在Docker中运行的PostgreSQL数据库,这个数据库叫traderdb
。现在,放回Xcode。通过Command和R或者左上角的运行按钮,编译并运行项目。等待Xcode运行。然后,确保控制台是打开的。你会看到一个NOTICE
,这代表了项目成功在http://127.0.0.1:8080
上运行。

小贴士:你可以忽略没有自定义工作目录的警告,这对本教程没有影响。如果你喜欢解决这个问题,阅读Vapor官方文档。
大功告成。现在,让我们学习Vapor中async/await的基础。
async/await和EventLoopFuture之间的桥梁
接下来你所有学习的东西,都通过两个简单而强大的工具实现。这允许开发者以最小的代价将他们的代码迁移到使用async/await,但是你还需要知道如何更好的使用它们。
将EventLoopFuture转换为async/await
第一个工具是EventLoopFuture
中的get()
函数,这个函数允许你通过async/await获得EventLoopFuture
中的值。考虑以下代码:
let usernameFuture: EventLoopFuture<String> = getUsernameFromDatabase()
现在,你可以通过异步的方式,获取函数的值。注意调用get()
:
let username: String = try await usernameFuture.get()
如你所见,get()
函数将EventLoopFuture<String>
变为了一个简单的String
值。
将async/await转换为EventLoopFuture
问题来了,如果你需要与get()
函数相反的功能,怎么办?有时,你没有一部分代码的控制权,或者你希望推迟迁移时间。同时,你需要与使用async/awiat部分的代码协作。
这就是第二个工具派上用场的时候。想象你需要将下面的async/await代码转变为返回EventLoopFuture
的代码:
let email: String = try await getUserEmailAsync()
在Vapor中,如果你可以访问一个EventLoop
,那么你可以很简单的实现:
let emailFuture: EventLoopFuture<String> = eventLoop.performWithTask {
return try await getUserEmailAsync()
}
在上述代码中,首先,你只需调用performWithTask(_:)
函数,这个函数可以在任何EventLoop
上使用。然后,在该函数的闭包中运行你的async
代码,最后,返回这个异步代码的最终值(结果)。
这非常简单,但是更好的消息时,大部分Vapor核心包已经完成了对async/await的支持。大多时候,你不需要使用这两个函数。:]
理解以同步上下文的方式运行异步任务
所有async/await函数必须在异步上下文中调用。这意味着,你不可以运行async/await在任何非async
函数中:

但是,一些时候你需要绕过这个限制。所以,这就是Task
存在的原因。在Swift中,Task
是一个异步环境,可以在任何地方使用。你可以简单的创建一个Task
实例,并运行你的异步代码。

如你所见,Task
也有priority
参数。你可以为你要运行的异步任务设置不同的优先级,Swift会基于这个运行你的异步任务。
最好不要使用Task
,因为SwiftNIO在未来会更少的控制你的异步操作。在Swift5.5,Swift自己管理async/await。但是不久之后,SwiftNIO会替代它。那时候你会感谢自己不随意创建Task
:]
事实上,现在SwiftNIO没有完全控制async
操作的执行,这会导致一个问题:你的async/await代码会比EventLoop
代码稍微慢一点。
在Vapor中使用async/await
让我们再回去查看示例项目。打开位于Controllers文件夹的CrateController.swift
文件。其中位于CrateController
的boot(routes:)
函数是用来设置Api的:
func boot(routes: RoutesBuilder) throws {
let crateGroup = routes.grouped("crates")
// 1
crateGroup.get(use: all)
// 2
crateGroup.post(use: create)
// 3
crateGroup.post("trade", use: trade)
}
在上面的代码中,有三个Api:
GET
HTTP请求,在 /crates获取所有交易POST
HTTP请求,在 /crates添加新交易POST
HTTP请求,在 /crates/trade实现(允许)交易
再来看看all(req:)
函数,尝试将其转换为使用async/await书写:
private func all(req: Request) throws -> EventLoopFuture<[Crate]> {
Crate.query(on: req.db).all()
}
基于你现在所学,你可以在创建(Create.query)请求上使用get()
函数,然后就会以async/await的方式运行了。同样要给函数加上async
标签。这样函数将会变成一个async throwing
函数并返回[Crate]
而不是仅仅普通的一个返回EventLoopFuture<[Crate]>
的可抛出错误函数:
private func all(req: Request) async throws -> [Crate] {
try await Crate.query(on: req.db).all().get()
}
再次编译并运行。这时候,所有问题都清楚了。第一步转换现在已经大功告成。
但是,先前我们说过这个小帮手并不时常需要使用。所以删除get()
函数的调用:
try await Crate.query(on: req.db).all()
再次编译并运行。你会看见Xcode成功运行了项目,并且没有因为通过async的方式使用all()
而抛出错误。为什么会这样?
为了更加清楚与方便的使用async/await,Vapor团队为所有常用的返回EventLoopFuture
的函数提供了async重载。这意味着,你可以使用两种all()
函数。原始的返回EventLoopFuture
,而新的是一个async
函数。在上述代码中,你使用了async/await的all()
重载函数,所以Swift认为这没有问题。
对于crateGroup.get(use: all)
也是如此。你已经将all(req:)
从一个返回EventLoopFuture
的函数变为了返回一个async
值。Vapor团结已经为所有路由创建器提供了async
重载函数。所以,Swift会自动使用async/await模式的get(use:)
,并不会抛出错误。
这就是Vapor支持async/await的幕后。多数使用EventLoopFuture
的函数现在已经可以使用async/await方式调用,易如反掌。
现在,让我们将create(req:)
转换为使用async/await。create(req:)
通过你发送的数据创建新的交易到数据库。他看起来是这样的:
private func create(req: Request) throws -> EventLoopFuture<[Crate]> {
let crates = try req.content.decode([Crate].self)
return crates.create(on: req.db).transform(to: crates)
}
通过你所学的,你需要两个小步骤:
- 将函数申明由
-> EventLoopFuture<Value>
改为async throws -> Value
- 在内部使用async/await的变体函数
转换之后,你的代码看起来像这样:
private func create(req: Request) async throws -> [Crate] {
// 1
let crates = try req.content.decode([Crate].self)
// 2
try await crates.create(on: req.db)
// 3
return crates
}
这是每一步的意义:
- 解码发送给你的数据
- 将解码的数据储存到PostgreSQL数据库
- 返回数据
目前为止,你已经学习了Vapor中async/await的基本用法。稍后你会返回这里,并学习一些进阶用法。
在Fluent上顺滑的使用
Fluent作为Vapor核心库之一,同样提供了async/await支持。这意味着你可以随时用async
在任意请求上。
你已经通过all(req:)
和create(req:)
函数创建了几个不同的Fluent请求。现在,是时候了解如何在迁移(migration)上使用async/await了。
更新Fluent迁移
在Fluent迁移中使用async/await非常简单,只有一点不同。打开位于Migrations文件夹的CreateCrate.swift
,你会看到以下代码:
struct CreateCrate: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database
.schema(Crate.schema)
.id()
.field(Crate.FieldKeys.owner, .string, .required)
.field(Crate.FieldKeys.item, .string, .required)
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database
.schema(Crate.schema)
.delete()
}
}
现在,你可以非常方便的将它们变为使用async/await。你的代码最终应该是这样的:
struct CreateCrate: Migration {
func prepare(on database: Database) async throws {
try await database
.schema(Crate.schema)
.id()
.field(Crate.FieldKeys.owner, .string, .required)
.field(Crate.FieldKeys.item, .string, .required)
.create()
}
func revert(on database: Database) async throws {
try await database
.schema(Crate.schema)
.delete()
}
}
尝试编译并运行。Xcode会抛出CreateCrate
不符合Migration
协议的错误:

这是什么原因?答案非常简单。Migration
协议需要prepare(on:) -> EventLoopFuture<Void>
和revert(on:) -> EventLoopFuture<Void>
,但是你将代码变为了async throws
。
所以Vapor团队想出了一个简单的解决办法。他们提供了AsyncMigration
协议。这个协议和Migration
类似,但是它允许async
的prepare(on:)
和revert(on:)
函数。
现在,将Migration
替换为AsyncMigration
:
struct CreateCrate: AsyncMigration {
...
AsyncMigration
本身符合Migration
协议,所以CreateCrate
依旧符合Migration
协议。这一位你可以在无需任何更改的情况下使用CreateCrate
,你不需要再去修改任何代码了!
编译并运行。你会发现没有任何错误,整个项目成功运行了。
总的来说,如果你之前使用的协议或者类型使用了EventLoopFuture
,而你现在希望将它们变为使用async/await。那通常来讲,你只需要加上Async
前缀即可。举个例子,AsyncMigration
就是Migration
的async/await版本,所以它有一个Async
前缀。
理解并发
是时候,大展身手将其他路由变为asycn/await代码了。不过,你现在的知识会让你的转换并不理想。
打开CrateController.swift
并查看tradeOne(on:tradingSides:)
:
private func tradeOne(
on db: Database,
tradingSides: TradeItem
) -> EventLoopFuture<HTTPStatus> {
// 1
Crate.query(on: db)
.filter(\.$id ~~ [tradingSides.firstId, tradingSides.secondId])
.all()
.tryFlatMap { bothCrates in
// 2
guard bothCrates.count == 2 else {
throw Abort(.badRequest)
}
let crate1 = bothCrates[0]
let crate2 = bothCrates[1]
// 3
(crate1.owner, crate2.owner) = (crate2.owner, crate1.owner)
// 4
let saveCrate1 = crate1.save(on: db)
let saveCrate2 = crate2.save(on: db)
// 5
return saveCrate1.and(saveCrate2).transform(to: .ok)
}
}
在上述代码中:
- 你获取了所有交易。记住,
TradeItem
只是两个id
的容器,它们的物品将会通过App交易。这个查询会找到所有有这两个id
的交易。 - 确保返回了两个交易,然后再给每个单独分配一个常量。
- 之后,交换交易中的项目。
crate1
会有crate2
的项目,反之亦然。这就是交易的发生地。 - 然后,储存新的交易到数据库中。
- 最后,返回保存到结果,并将其转化为
200 OK
的HTTP状态。
现在,你要将tradeOne(on:tradingSides:)
转换为使用async/await。转换之后,你的代码应该看起来像这样:
private func tradeOne(
on db: Database,
tradingSides: TradeItem
) async throws -> HTTPStatus {
// 1
let bothCrates = try await Crate.query(on: db)
.filter(\.$id ~~ [tradingSides.firstId, tradingSides.secondId])
.all()
// 2
guard bothCrates.count == 2 else {
throw Abort(.badRequest)
}
let crate1 = bothCrates[0]
let crate2 = bothCrates[1]
// 3
(crate1.owner, crate2.owner) = (crate2.owner, crate1.owner)
// 4
try await crate1.save(on: db)
try await crate2.save(on: db)
// 5
return .ok
}
在此过程中,你做了以下事情:
- 获取了所有有关交易。
- 确保获得了两个,并依次分配个变量。
- 交换项目。
crate1
有了crate2
项目,反之亦然。这时候就发生了交易。 - 保存交易到数据库。
- 返回
200 OK
的HTTP状态。
编译并运行,你会发现trade(req:)
抛出了有关tradeOne(on:tradingSides:)
的错误。这非常正常,你刚刚把tradeOne(on:tradingSides:)
转换为了async/await函数,但是你忘记转换trade(req:)
了:
所以,你需要将它也转换为async/await函数,现在你的代码看起来是这样的:
private func trade(req: Request) throws -> EventLoopFuture<HTTPStatus> {
// 1
let allTrades = try req.content.decode([TradeItem].self)
// 2
return allTrades.map { tradingSides in
tradeOne(on: req.db, tradingSides: tradingSides)
}
// 3
.flatten(on: req.eventLoop)
// 4
.transform(to: .ok)
}
下面我们来解释一下这段代码:
- 解码收到的
TradeItem
数组。 - 遍历
allTrades
,然后对每个TradeItem
使用tradeOne(on:tradingSides:)
。 - 现在,你有个存满
EventLoopFuture<Void>
的数组,你将它们展开,变为一个EventLoopFuture<Void>
,这个EventLoopFuture<Void>
包含了所有交易结果。 - 最后,你将它们转化为
200 OK
的HTTP状态。
尝试将它们变为使用async/await。之后,你的代码应该是这样的:
private func trade(req: Request) async throws -> HTTPStatus {
// 1
let allTrades = try req.content.decode([TradeItem].self)
// 2
for tradingSides in allTrades {
_ = try await tradeOne(on: req.db, tradingSides: tradingSides)
}
// 3
return .ok
}
在上述代码中,你做了以下几件事:
- 解码收到的
TradeItem
数组。 - 遍历
allTrades
,然后对每个TradeItem
使用tradeOne(on:tradingSides:)
。你并不需要tradeOne(on:tradingSides:)
的结果,你需要关系的是有没有抛出错误。 - 最后,返回
200 OK
的HTTP状态。
看起来转换trade(req:)
和tradeOne(on:tradingSides:)
非常简单。你或许没有注意到,async/await的代码与EventLoopFuture
在执行上有些些不同。
理解通过TaskGroup实现并发
再次查看trade(req:)
,然后与旧代码比较一下:
这是使用EventLoopFuture
的旧代码:
allTrades.map { tradingSides in
tradeOne(on: req.db, tradingSides: tradingSides)
}
.flatten(on: req.eventLoop)
这是使用async/await的新代码:
for tradingSides in allTrades {
_ = try await tradeOne(on: req.db, tradingSides: tradingSides)
}
现在有个问题,使用EventLoopFuture
代码是同时运行的,而async/await代码再是一个接一个运行的。EventLoopFuture
代码自创建就启动了,但是async/await代码中,每个循环都会遇到await
,这会停止并等待直到结果返回。这就导致async/await变得非常慢。
解决方法是使用TaskGroup
。TaskGroup
能让所有任务通过并发的方式完成。你需要通过withTaskGroup(of:returning:body:
或withThrowingTaskGroup(of:returning:body:)
来使用TaskGroup
。这两个函数的区别是第二函数会给你一个ThrowingTaskGroup
,这个TaskGroup
的addTask(priority:operation:)
函数允许抛出错误。ThrowingTaskGroup
正如其名,是一个允许抛出错误的TaskGroup
变体。
所以更好的转换是:
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
}
}
在这里,你做了如下事情:
- 解码收到的
TradeItem
数组。 - 通过
withThrowingTaskGroup(of:returning:body:)
创建一个新的TaskGroup
。of
参数代表了addTask(priority:operation:)
将要返回的值。你会注意没有使用到returning
参数,这是因为Swift自动推断出了TaskGroup
会返回HTTPStatus
。 - 遍历任务,并为每一个
TaskGroup
通过addTask(priority:operation:)
向TaskGroup
添加新的任务。priority
可以使用默认值,也就是不填写。 - 等待所有任务完成。一个非常重要的点是,即使没有
waitForAll()
,Swift依然会等待所有任务完成。不过,如果没有了它,那么tradeOne(on:tradingSides:)
抛出错误的时候将不会通知你。 - 返回
200 OK
的HTTP状态。
注意,你可以遍历TaskGroup
中的值,并获取每个任务的结果。你通过可以在TaskGroup
中使用next()
函数,或者使用便利:
for await taskResult in taskGroup {
// do something with the `taskResult`
}
幸运的是,你不需要返回值,所有不需要去获取它们。
理解通过async let并发
现在,你“修复”了trade(req:)
函数,查看tradeOne(on:tradingSides:)
,它包含以下两行:
try await crate1.save(on: db)
try await crate2.save(on: db)
这是另外一个不太理想的代码!每个交易保存的时候都会等待一次,二这两个操作并不互相依赖。为了解决这个问题,你任然可以使用TaskGroup
,不过这样就杀鸡用牛刀了。最好的方式是将这行代码替换成下面的:
async let crate1Saving: Void = crate1.save(on: db)
async let crate2Saving: Void = crate2.save(on: db)
_ = try await (crate1Saving, crate2Saving)
你会注意到,save(on:)
是一个async
函数,但是这里没有await
关键词,而是变为了async let
。async
变量是一个Swift的新功能。这允许将async
函数的值赋值给一个变量,并且没有异步等待。这就是上面代码为什么没有await
的原因。你讲两个保存操作的值赋值给crate1Saving
和crate2Saving
,然后你推迟了await
操作到另外的地方。
最后一行是等待两个进程都完成了再继续。在这里,你将保存进度赋值给了一个定义为(crate1Saving, crate2Saving)
的元组。然后使用try await
等待他们同时完成。_ =
的原因是你不需要这个操作的结果。
对你来说,TaskGroup
和async
是最好的解决办法,你当然可以使用Task.detached(priority:operation:)
运行并发代码。你需要在下次再学习这个知识。:]
下一步?
你可以下载位于本教程顶端/底部的示例项目。
译者注:请前往https://www.raywenderlich.com/29095914-async-await-in-server-side-swift-and-vapor下载示例项目
在这个教程中,你学习了Vapor中最重要的异步知识,不过学无止境,你还有很多要学。
学习更多新的并发API,查看Modern Concurrency in Swift。
如果你需要了解更多的Vapor新的async/await API,查看官方的Vapor documentation。
我们希望你可以享受这个教程,如果你有任何问题可以前往原文评论,或者前往论讨论。
本文翻译自:https://www.raywenderlich.com/29095914-async-await-in-server-side-swift-and-vapor
转载本文请包含版权信息