Async-异步处理
Vapor 3最重要的新功能之一是(Async)异步处理, 但也可能是最令人困惑的一个功能。 为什么它如此重要呢?
想象一下您的服务器只有一个线程但是有四个客户端需要请求资源的情况,请求顺序如下:
- 对股票报价信息的请求。 这需要在另一台服务器上调用API来获取结果。
- 对静态CSS样式表的请求。 CSS无需查找即可立即使用并返回结果。
- 对用户用户资料的请求。 必须从数据库中获取用户信息资料。
- 对一些静态HTML的请求。 HTML无需查找即可立即使用并及时向客户端返回结果。
在同步服务器(synchronous server)的处理中,服务器的唯一线程将一直阻塞,直到获得股票报价信息。 获取后它返回股票报价信息和CSS样式表信息。 但在数据库获取用户信息时其再次阻塞(因为需要查询数据库信息)。 只有完全获取到用户信息并返回结果后,服务器才会将静态HTML返回给客户端。
另一方面,在异步服务器(asynchronous server)中,线程启动调用去获取股票报价信息,因为其不能立即返回结果,于是就将此请求放在一边让其自己处理直到获得结果并返回。 但同时他会处理第二个请求并立即返回CSS样式表信息,紧接着启动数据库去获取用户信息(因为用户信息需要耗时查询数据库所以也需要放一边)并及时返回获取到的静态HTML。 当放在一边的请求处理(获取股票报价信息和查询数据库用户信息)完成后,线程将继续处理并将结果返回给客户端。
你可能会说,服务器有很多线程啊。的确是有不少!但是线程的数量虽多也是有上限的。同时在线程之间切换处理环境开销也是巨大的,并且还要确保所有数据访问都是线程安全的,这非常耗时且容易出错。 因此,尝试仅通过添加线程来解决问题是一种糟糕,且低效的解决方案。
Futures 和 Promises
为了在等待响应时“搁置”请求,您必须将请求包含在promise
中,以便在收到响应时恢复其运行。 实际上,这意味着您必须更改“搁置”的函数返回类型。
在同步的环境中,一个函数通常可以像这样书写:
func getAllUsers() -> [User] {
//do sth db queries
}
在异步环境中,这种方式是不行的,因为在getAllUsers()
必须返回时,您的数据库调用可能尚未完成。 你知道你将来能够返回[User]
,但现在不行。 在Vapor中,您承诺(promise
)提供结果称为未来(Future
)。 所以你的代码应该是这样:
func getAllUsers() -> Future<[User]> {
//do sth db queries
}
使用Future去处理任务
Unwrapping futures
Vapor具有许多convenience functions是和future
一起使用的。 但是,有很多场景需要使用future
并等待promise
执行。 为了演示,假设您有一条返回HTTP状态代码204 No Content的路由。 此路由使用类似上述功能从数据库中获取用户列表,并在返回之前修改列表中的第一个用户。
为了使用该调用的结果,您必须unwrapp结果并提供一个闭包,以便在Future解析时执行。 您将使用两个主要函数来执行此操作:
-
flatMap(to:)
: 用于当promise闭包返回Future
类型时使用。 -
map(to:)
: 当promise闭包返回Future以外的类型时使用。
// 1
return database
.getAllUsers()
.flatMap(to: HTTPStatus.self) { users in
// 2
let user = users[0]
user.name = "Bob"
// 3
return user.save().map(to: HTTPStatus.self) { user in
//4
return HTTPStatus.noContent
}
}
- 从数据库中获取所有用户。 如上所述,
getAllUsers()
返回Future <[User]>
。 由于完成此Future
的结果是另一个Future
(参见步骤3),因此使用flatMap(to :)
来解包结果。flatMap(to :)
的闭包接收完成的Future
-[User]
作为参数。 这个.flatMap(to :)
返回Future
。 - 更新第一个用户的名称。
- 将更新的用户保存到数据库。 这将返回
Future
,但您需要返回的HTTPStatus
值并不是Future
,因此需使用map(to :)
。 - 返回适当的
HTTPStatus
值。
Transform
有时你不关心Future的结果,只关心它是否成功。 在上面的示例中,你不想使用save()
的结果并对其解包操作。 对于此场景,您可以使用transform(to :)
简化步骤3:
return database
.getAllUsers()
.flatMap(to: HTTPStatus.self) { users in
let user = users[0]
user.name = "Bob"
return user.save().transform(to: HTTPStatus.noContent)
}
这有助于减少代码的嵌套量,并使代码更易于阅读和维护。
Flatten
有时您必须等待一些Futures
完成。 比如在数据库中保存多个模型时。 在这种情况下,您使用flatten(on:)
。 例如:
static func save(_ users: [User], request: Request)
-> Future {
// 1
var userSaveResults: [Future] = []
// 2
for user in users {
userSaveResults.append(user.save())
}
// 3
return userSaveResults.flatten(on: request)
//4
.transform(to: HTTPStatus.created)
}
- 定义一个
Future
的数组,即第二步中save()
的返回类型。 - 循环遍历
users
数组中的每个用户,并将user.save()
的返回值添加到数组中。 - 使用
flatten(on :)
等待所有future
完成。 这需要一个Worker,即实际执行任务的线程。 worker通常是Vapor中的请求。 如果需要,flatten(on :)
的闭包将返回的collection作为参数。 - 返回201 Created状态。
flatten(on :)
等待所有future返回,因为它们是由同一个Worker异步执行的。
Multiple futures
有时候,你需要等待一些不相互依赖的不同类型的Future
。 例如,在解码请求数据并从数据库中获取用户时会遇到这种情况。 Vapor提供了许多全局的convenience函数,可以等待多达五种不同的future。 这有助于避免代码的深层嵌套或令人困惑的链式书写。
如果你有两个future-从数据库中获取用户列表,及从请求中解码一些数据,你可以这样做:
// 1
flatMap(
to: HTTPStatus.self,
database.getAllUsers(),
// 2
request.content.decode(UserData.self)) { allUsers, userData in
// 3
return allUsers[0]
.addData(userData)
.transform(to: HTTPStatus.noContent)
}
- 使用全局
flatMap(to:_:_ :)
等待两个future完成。 - 闭包将完成的
future
作为参数. - 调用
addData(_ :)
,它返回一些future
的结果并将返回类型transform
为.noContent
。
如果闭包返回非future
结果,则可以使用全局map(to:_:_ :)
代替:
// 1
map(
to: HTTPStatus.self,
database.getAllUsers(),
// 2
request.content.decode(UserData.self)) { allUsers, userData in
// 3
allUsers[0].syncAddData(userData)
// 4
return HTTPStatus.noContent
}
- 运用全局函数
map(to:_:_:)
去等待两个Future
完成。 - 这个闭包将两个处理完成的
futures
作为参数。 - 调用同步函数
syncAddData(_:)
; - 返回
.noContent
;
创建 Futures
有时你需要创建自己的Future
。 如果if语句返回类型不是Future
,而else返回的类型是Future
,编译器会抛出错误(这些返回类型必须一致)。 要解决此问题,您必须使用request.future(_ :)
将非Future
类型转换为Future
类型。 例如:
// 1
func createTrackingSession(for request: Request)
-> Future {
return request.makeNewSession()
}
// 2
func getTrackingSession(for request: Request)
-> Future {
// 3
let session: TrackingSession? =
TrackingSession(id: request.getKey())
// 4
guard let createdSession = session else {
return createTrackingSession(for: request)
}
// 5
return request.future(createdSession)
}
- 定义一个从请求中创建
TrackingSession
的函数。 这将返回Future
。 - 定义一个从请求中获取
Tracking Session
的函数。 - 尝试使用请求的
key
创建Tracking Session
。 如果无法创建Tracking Session
,则返回nil
。 - 确保
Session
已成功创建,否则创建新的Tracking Session
。 - 使用
request.future(_ :)
从createdSession
中创建Future
。 这将返回运行请求的同一个Worker上的Future
。
由于createTrackingSession(for :)
返回Future
,您必须使用request.future(_ :)
将createdSession
转换为Future
以使编译器不出现报警异常。
错误的处理
Vapor在其整个框架中大量使用Swift的错误处理。 许多函数throws
,允许您处理不同级别的错误。 您可以选择在route handlers
内处理错误,或者使用中间件(middleware)来捕获更高级别的错误,或两者兼而有之。
但是,在异步世界中处理错误有点不同。 你不能使用Swift的do/catch
,因为不知道什么时候会执行do/catch
。 Vapor提供了许多函数来帮助处理这些错误。 在基本层面上,Vapor有自己的do/catch
回调函数与Futures
一起使用:
let futureResult = user.save()
futureResult.do { user in
print("User was saved")
}.catch { error in
print("There was an error saving the user: \(error)")
}
在Vapor中,您必须在处理请求时返回一些内容,即使它的类型是future
。 使用上面的do/catch
方法不会阻止错误的发生,但它会让你看到错误是什么。 如果save()
调用失败并返回futureResult
,则失败仍然会沿着调用链向上传播。 但是,在大多数情况下,您希望尝试纠正此问题。
Vapor提供了catchMap(_ :)
和catchFlatMap(_ :)
来处理这种类型的failure。 这允许您处理错误(error),并修复它或抛出不同的错误(error)。 例如:
// 1
return user.save(on: req).catchMap { error -> User in
// 2
print("Error saving the user: \(error)")
// 3
return User(name: "Default User")
}
- 尝试保存用户。 如果出现错误,提供
catchMap(_ :)
来处理这个错误。此闭包将error
作为参数,并且必须返回已解析的future
的类型 - 在本例中为User
。 - 打印出收到的错误信息。
- 创建一个默认的用户实例并返回。
当相关联的闭包返回Future
时,Vapor还提供相关的catchFlatMap(_ :)
方法:
return user.save().catchFlatMap { error -> Future in
print("Error saving the user: \(error)")
return User(name: "Default User").save()
}
由于save()
返回future
,因此必须调用catchFlatMap(_ :)
。
catchMap
和catchFlatMap
只在失败时执行它们的闭包。 但是如果你想要同时处理错误并处理成功案例呢? 简单! 只需链式调用适当的方法即可!
future的链式调用
处理future
有时在使用时很容易得到嵌套多层深度的代码。 Vapor允许您将future
链式调用而不是多层嵌套的使用它们。 例如,考虑一个如下所示的代码段:
return database
.getAllUsers()
.flatMap(to: HTTPStatus.self) { users in
let user = users[0]
user.name = "Bob"
return user.save().map(to: HTTPStatus.self) { user in
return HTTPStatus.noContent
}
}
方法map(to:)
和flatMap(to:)
可以一起链式使用。
return database
.getAllUsers()
// 1
.flatMap(to: User.self) { users in
let user = users[0]
user.name = "Bob"
return user.save()
// 2
}
.map(to: HTTPStatus.self) { user in
return HTTPStatus.noContent
}
更改flatMap(to:)
的返回类型允许链式调用map(to:)
,它接收Future
。 最终的map(to:)
返回你最初要返回的类型。
future
链式调用允许您减少代码中的嵌套,并且可以使其更容易理解,这在异步世界中尤其有用。 然而,无论你是嵌套使用或链式调用这完全是个人喜好。
Always
有时无论future
的结果如何, 你都想要执行一些事情。 您可能需要关闭连接,触发通知或仅记录future
的执行。 对于这些使用always
回调进行处理。 例如:
// 1
let userResult: Future = user.save()
// 2
userResult.always {
// 3
print("User save has been attempted")
}
- Save a user and set the result to
userResult
. This is of typeFuture
. - Chain an
always
to the result. - Print a string when the app executes the future.
无论是future的结果是失败还是成功,闭包always
都会被执行。 它对future
也没有影响。 您也可以将其与其他方法一起链式调用。
Waiting
在某些情况下,您可能希望实际等待结果返回。 为此可以使用wait()
。
注意:这有一个很大的警告:你不能在主事件循环上使用
wait()
,这意味着所有请求处理程序和大多数其他情况不能使用wait()
。
但是,这个方法在测试中尤其有用,因为编写异步测试很困难。 例如:
let savedUser = try user.save(on: connection).wait()
savedUser
的类型不是Future
,因为您使用的是wait()
,在这个实例中savedUser
是一个User
对象。请注意,如果执行promise
失败,wait()
将引发错误。
值得重申的是:不能在主事件循环内使用
wait()
!
翻译自Raywenderlich--Server Side Swift with Vapor