Async 概览
可能你已经注意到Vapor中的一些API会期望或返回一个通用的Future类型。对于第一次听说futures的人起初会存在些困惑但别担心,Vapor会让它们变得很易用。
Promises 和 futures是有关联的,但也是截然不同的类型。Promises 用来创建futures。通常,你将使用Vapor的APIs所返回的future来工作从而不必担心创建promises。
type | description | mutability | methods |
---|---|---|---|
Future | 引用一个可能还是不可用的对象 | 只读 | .map(to::) .flatMap(to::) do(:) catch(:) |
Promise | 提供一些异步的对象 | 可读写 | succeed(:) fail(:) |
Future
是基于回调的异步API的替代品,Futures可以链式以及转化来使用,这是简单闭包无法企及的方式,这使得Futures非常强大。
Transforming
就像Swift中的可选项,futures可以被mapped
以及flat-mapped
。这些是你在futures上执行的最常见操作
method | signature | description |
---|---|---|
map | to: U.Type, _: (T) -> U | 把一个future Map成一个不同的值 |
flatMap | to: U.Type, _: (T) -> Future<U> | 把一个future Map成一个不同的future值 |
transform | to: U | 将一个future Map成一个已经可用值 |
如果你查看Optional<T>
和Array <T>
上的map
和flatMap
的方法签名,您将看到它们与Future <T>
上的方法非常相似
Map
.map(to:_:)
方法允许你将 future 的值转换成另一个值。一旦 Future
的数据准备就绪后,提供的闭包将会立即被调用。
/// 设想我们从某个API获取了一个future string
let futureString: Future<String> = ...
/// 将这个 future string转为 integer
let futureInt = futureString.map(to: Int.self) { string in
print(string) // 实际的string
return Int(string) ?? 0
}
/// 现在我们有了一个 future integer
print(futureInt) // Future<Int>
Flat Map
.flatMap(to:_:)
方法允许你将 future 的值转换成另一个 future 值。因为 flatMap 可以使你避免生成内嵌的 futures(比如:Future<Future<T>>
),所以以 "flat" 来命名。换言之,这将使得你的 更加扁平化。
/// 假设我们从某个API获取一个 Future<String> 对象
let futureString: Future<String> = ...
/// 假设我们创建了一个http客户端
let client: Client = ...
/// Flat-map 操作将Future<String>转为Future<Response>
let futureResponse = futureString.flatMap(to: Response.self) { string in
return client.get(string) // Future<Response>
/// 现在我们就有了一个Future<Response>
print(futureResponse) // Future<Response>
提示: 上述例子中如果使用 .map(to:_:) 替代的话,我们将会最终得到
Future<Future<Response>>
。
Transform
.transform(_ :)
方法允许你修改future's value,而忽略现有值。这对于转化那些对于future的实际价值并不重要的Future <Void>
类型的结果特别有用。
提示:
Future<Void>
,有时被称为信号,其唯一目的是通知你一些异步操作的完成或失败。
/// 假设我们从某个API获取了一个 future<void>
let userDidSave: Future<Void> = ...
/// 将其转化为一个http状态
let futureStatus = userDidSave.transform(to: HTTPStatus.ok)
print(futureStatus) // Future<HTTPStatus>
这个操作仍然是一个转变,即使我们已经提供了一个可使用的值用来transform.
在之前的futures还未完成(或者失败)之前,这个future不会完成。
Chaining
Futures 有这么一个特性,转换可以被 chained 。它允许你可以很容易的进行多次转换。
让我们修改上述示例:
/// 假设我们获取一个future返回值从某个api
let futureString: Future<String> = ...
/// 假设我们已经初始化了一个客户端
let client: Client = ...
/// 先转化为一个URL再转化为一个Response类型。
let futureResponse = futureString.map(to: URL.self) { string in
guard let url = URL(string: string) else {
throw Abort(.badRequest, reason: "Invalid URL string: \(string)")
}
return url
}.flatMap(to: Response.self) { url in
return client.get(url)
}
print(futureResponse) //Future<Response>
在初次调用 map 后,生成了个临时的 Future<URL>
。然后该 future 立即被 flat-mapped 成 Future<Response>
。
提示: 你可以在 map 和 flat-map 闭包中
throw
错误。这将引起 future 失败,并附带抛出来的错误信息。
/// Assume we have a Request and some ViewRenderer
let req: Request = ...
let view: ViewRenderer = ...
/// Render the view, using the Request as a worker.
/// This ensures the async work happens on the correct event loop.
///
/// This assumes the signature is:
/// func render(_: String, on: Worker)
view.render("home.html", on: req)
Futures
因为 Future
是异步执行的,我们必须使用闭包来进行交互并转换它们的值。就像 Swift 中的 optionals 一样, futures 可以被 mapped 和 flat-mapped 。
Do / Catch
与Swift的do / catch语法类似,futures也有一种do和catch方法来等待未来的结果。
/// Assume we get a future string back from some API
let futureString: Future<String> = ...
futureString.do { string in
print(string) // 实际值
}.catch { error in
print(error) // 一个Swift类型的错误
}
提示:
.do
和.catch
一起工作。如果你忘记.catch
,编译器会警告你一个未使用的结果。不要忘记处理错误的情况!
Always
你可以使用always
来添加一个无论是失败还是成功都会被调用的回调。
/// Assume we get a future string back from some API
let futureString: Future<String> = ...
futureString.always {
print("The future is complete!")
}
Wait
你可以使用.wait(
)同步等待future完成。由于future可能会失败,所以这个调用会抛出异常。
/// Assume we get a future string back from some API
let futureString: Future<String> = ...
/// 一直阻塞到string操作完成
let string = try futureString.wait()
print(string) /// String
注意: 不要在一个vapor 的路由或者控制器中使用这个方法
Promise
大多数情况下,你是通过调用vapor的API's来获取一个需要操作的future。但是,在某些时候,你可能需要创建自己的promise。
要创建promise,你需要访问EventLoop
。Vapor中的所有容器都有一个可以使用的eventLoop
属性。最常见的是,就是当前的Request
。
/// Create a new promise for some string
let promiseString = req.eventLoop.newPromise(String.self)
print(promiseString) // Promise<String>
print(promiseString.futureResult) // Future<String>
/// Completes the associated future
promiseString.succeed(result: "Hello")
/// Fails the associated future
promiseString.fail(error: ...)
提示: 一个Promise对象只能够被完成一次,所有的后续完成操作都会被忽略。
线程安全
Promises可以从任何线程完成(succeed(result:) / fail(error:))
。这就是为什么promise需要一个事件循环来初始化。Promise确保完成操作在它的事件循环执行。
Event Loop (事件循环)
它通常会为其运行的CPU中的每个内核创建一个事件循环在你的应用程序启动时。每个事件循环只有一个线程。如果你熟悉Node.js的事件循环,就会发现Vapor中的事件循环与Node.js中的非常相似。唯一的区别是Vapor可以在一个进程中运行多个事件循环,因为Swift支持多线程。
每次客户端连接到你的服务器时,它都将被分配给其中一个事件循环。从这一点开始,服务器和该客户端之间的所有通信都将发生在同一个事件循环(并关联到这个事件循环的线程)上。
事件循环负责跟踪每个连接的客户端的状态。如果有来自客户端的请求等待读取,则事件循环触发读取通知,导致数据被读取。一旦读取完整个请求,任何等待该请求数据的futures都将完成。
Worker
你也许会看到 Vapor 很多方法中都有一个 on: Worker
参数。这些方法通常是要执行异步任务,而且要访问 EventLoop
Vapor 中常见的 Workers 有如下几个:
- Application
- Request
- Response
你可以在这些容器上使用.eventLoop属性来访问事件循环。
print(app.eventLoop) // EventLoop
Vapor中有许多方法需要传递当前worker。它通常会有像on: Worker
这样的标签。如果你处于路由闭包或控制器中,请传递当前Request
或Response
。如果你在启动应用程序时需要一个worker,请使用Application
Blocking(阻塞)
绝对关键的规则如下
危险
切勿直接在事件循环中进行阻塞调用。
一个阻塞调用的例子,类似于libc.sleep(_ :)
.
router.get("hello") { req in
/// 使当前事件循环阻塞5s
sleep(5)
///当线程重新工作室返回一个简单字符串
return "Hello, world!"
}
sleep(_ :)
是一个用于在提供的秒数内阻止当前线程的命令。如果你直接在事件循环中阻塞工作,则在阻塞工作期间,事件循环将无法响应分配给它的任何其他客户端。换句话说,如果你在事件循环中进行睡眠(5),则连接到该事件循环的所有其他客户端(可能为数百或数千个)将被延迟至少5秒
确保只有在后台才运行任何阻塞工作。当这项工作以非阻塞方式完成时,使用Promise通知事件循环。
router.get("hello") { req in
/// 创建一个空的promise
let promise = req.eventLoop.newPromise(Void.self)
/// 在后台线程执行
DispatchQueue.global() {
/// 使后台线程睡眠
/// 这不会影响到当前的事件循环
sleep(5)
/// 当阻塞工作完成时
/// promise和他关联的future会被完成
promise.succeed()
}
/// 等待 future完成
/// 将结果转为一个简单的字符
return promise.futureResult.transform(to: "Hello, world!")
}
并非所有的阻塞调用会像sleep(:)
如此明显,当你怀疑你调用的方法可能被阻塞时,研究下这个方法本身或者询问相关的人。如果该函数正在执行磁盘或者网络IO并且使用的是一个同步API(无回调或者futures),那么就会发生阻塞的情况。
提示: 如果阻塞工作是应用程序的核心部分,则应该考虑使用BlockingIOThreadPool来控制你创建的阻塞工作的线程数。当阻塞工作正在被完成时,这将避免你的事件循环长期获取不到 CPU 时间片。。
网友评论