Async
是一个免费使用的函数和类型框架,我发现在使用Grand Central Dispatch时非常有用。尽管该库的核心是 Future
及其对应的 Promise
,但它们在更广泛的上下文中也很有用。 我长期以来一直在我的代码中使用它们,并且一直想分享它们,所以现在我终于这么做了。 我还有其他发现有用的类型和函数,我可能会在以后添加,但这些构成了我几乎每次使用 GCD 时使用的核心功能。
即使 Future
才是真正的核心特性,该库还是被称为 Async
,因为它提供了全局的 async
自由函数,并在 DispatchQueue
上添加了相应的返回 Future
的方法,因此我几乎不需要显式地创建 Promise
,而且通常 Future
本身会消失在流畅的完成处理程序语法之后,以至于看起来该库似乎是关于 async
的。但实际上,Future
才是英雄,它比我见过的任何 Future
实现都更灵活。
我经常尝试使用命令行工具而不是实际的 AppKit/UIKit 应用程序来实现一个想法,因此该软件包专门设计为独立于这些。它仅使用 GCD 和 Foundation。
全局 async
自由函数有几种变体,它们会立即返回一个 Future
,异步执行你的闭包,该闭包可以安全地抛出错误,在全局默认并发 DispatchQueue
上执行。 你无需显式地创建队列,除非你需要它用于其他原因。
我提供了几种变体,因为我发现必须在 GCD 的 asyncAfter
中将截止日期指定为 DispatchTime
很烦人。 我几乎总是希望我的代码尽快运行,或者在调用 async
后的指定延迟后运行,几乎从不需要在特定时间点运行它。 因此,我提供了 async
函数的变体,允许你像 GCD 的原生版本一样将截止日期指定为 DispatchTime
,或者将延迟指定为 DispatchTimeInterval
或 TimeInterval
。
有全局 async
自由函数的同步版本。 它们只是立即在当前线程中运行你的闭包,就像你直接调用它一样。 这对于以下几个原因很有用
• 它允许在将同步代码重构为异步代码的过程中进行中间步骤,在此期间你仍然以同步方式执行闭包,同时从中获取 Future
。 在这种情况下,当你的闭包返回时,Future
会立即准备就绪,如果你添加了处理程序,它们会立即被调用。 然后,只需将 sync
更改为 async
,它就会变成真正的异步。
• 有时使用 sync
代替 async
进行调试会很有帮助,而且这是一个简单的一字母代码更改。
• 它返回一个 Future
,因此你可以在自己的代码中使用它,作为一种简单的方式来返回 Future
,而无需担心如何使用 Promise
,尽管 Promise
也很容易直接使用。
async
和 sync
方法已添加到 DispatchQueue
以对应于它们的全局版本,但是你可以在你选择的队列上获得 Future
返回行为,而不是默认的全局并发队列。
Mutex
是一个类,它通过提供 withLock
方法来简化对共享数据进行线程安全的保护,以自动锁定和解锁互斥锁,这应该是最常用的方法。 但是,认识到有时需要以并非总是可以方便地嵌套的方式交错锁定和解锁不同的互斥锁,它还提供了显式的 lock
、unlock
方法以及一个可失败的 tryLock
方法,允许你指定超时。
此实现允许你使用在大多数库中看到的典型流畅风格,但它也允许你将 Future
用作值的真正占位符,类似于 C++ 的 std::future
,你可以将其存储起来并在需要时查询。 它还允许你为这两种用法指定超时。
Promise
是 Future
的必要但通常隐藏的对应物。 即使它的接口非常简单,如果你主要只需要从 async
获取 Future
,你永远不需要自己创建 Promise
。 另一方面,你可以使用 Promise
从你喜欢的任何代码中返回 Future
,例如你自己的第三方异步库包装器。
尽管每个函数和类型都已使用标记注释进行了完整记录,因此在 Xcode 中,你可以通过 QuickLook 轻松获得参考,但是看到至少基本的示例作为起点是有帮助的。 我将按照我认为大多数人使用它们的顺序来介绍这些示例。
Future
孤立地看没有多大意义,因此我将结合 async
来描述它,但要理解从根本上讲,直接获得 Future
的唯一方法是从 Promise
,稍后将对此进行描述。 async
在内部使用 Promise
来返回 Future
。 这没有什么特别之处,你可以自己这样做以在其他上下文中使用 Future
。
你可以通过几种方式来考虑 Future
,而且它们都是同时成立的
Promise
是该通道的发送端。将处理程序(回调)附加到 Future
是防止你的代码阻塞结果的最简单和最安全的方法,因为你可以在附加它们后继续做其他事情,并且你的处理程序会在你的异步代码完成时自动调用。 就易用性和安全性而言,处理程序通常是首选方法。
但是,它们确实有一些缺点。 如果你要合并必须全部完成才能取得进展的多个异步任务的结果,处理程序可能会很尴尬。 例如,在计算图中,所有输入计算必须在当前节点可以评估之前完成。 在这种情况下,当前计算应阻塞,直到输入准备就绪。 当然,你可以将处理程序的方钉强行塞进这个圆孔中,但这不是处理程序的好用例。 你需要更新输入的一些计数器,确保从多个处理程序对该计数器的更新是原子性的,并且阻塞该计数器。 这并不难,但是多么不必要、低效且容易出错的混乱! 相比之下,将 Future
的集合用作占位符是理想的选择。 只需迭代地等待它们中的每一个。 当循环完成时,它们都已准备就绪,你可以继续评估当前节点。 这正是我提供替代方案的原因。
另一方面,从网络获取数据或响应事件,这是非常常见的情况,通常是处理程序的理想用途,而保留 Future
作为占位符会变得很尴尬,因为它需要你阻塞直到它准备就绪,或者编写某种循环或其他方案来持续检查它何时准备就绪,而你的代码继续执行。
话虽如此,完成处理程序非常适合回调,例如对于 HTTP 请求,将它们用作占位符是避免深度嵌套回调的好方法,当一个请求的结果需要生成另一个请求时,以此类推。
此软件包中 async
的实现与 GCD 的原生 async
和 asyncAfter
方法在两个方面有所不同。 第一个是它返回一个 Future
,第二个是除了 DispatchQueue
本身的方法之外,还有使用默认并发 DispatchQueue
的全局自由函数变体。
当你调用 async
时,它会安排你的闭包执行,使用 GCD 的原生 async
,但它也会立即返回一个 Future
,用于你的闭包将返回的值,或者它将抛出的错误。 你可以保留此 Future
作为查询你的闭包最终结果的一种方式,或者你可以使用它来附加处理程序...或者两者兼而有之。 这两种使用方式可以一起使用,如果这对你的应用程序有意义的话。
作为一个基本示例,假设我们有一个长时间运行的函数 foo() throws -> Int
。 我们可以使用 async
安排 foo
,并将处理程序附加到返回的 Future
,如下所示
async { return try foo() }.onSuccess {
print("foo returned \($0)")
}.onFailure {
print("foo threw exception, \($0.localizedDescription)")
}
foo
将并发运行,如果它最终返回一个值,则将使用该值调用传递给 .onSuccess
的闭包。 另一方面,如果 foo
抛出错误,则使用该错误调用传递给 .onFailure
的闭包。
请注意,Future
没有显式地出现在上面的代码中,但它就在那里。 它是 async
返回的内容,在本例中是 Future<Int>
,并且我们正在调用该 Future
的 .onSuccess
方法来指定成功处理程序。 .onSuccess
返回相同的 Future
,这使我们可以链接 .onFailure
方法调用来安排我们的失败处理程序。 它等效于
let future = async { return try foo() }
future.onSuccess {
print("foo returned \($0)")
}
future.onFailure {
print("foo threw exception, \($0.localizedDescription)")
}
你可以按任何顺序附加处理程序,如果你愿意先放置失败处理程序。
如果你更喜欢使用 Swift 的 Result
类型,则可以使用更通用的 .onCompletion
处理程序
async { return try foo() }.onCompletion {
switch $0 {
case let .success(value):
print("foo returned \(value)")
case let .failure(error):
print("foo threw exception, \(error.localizedDescription)")
}
}
你可以根据需要指定任意数量的处理程序,混合和匹配完成处理程序、成功处理程序和失败处理程序。 所有适用的处理程序都将并发调用。 这允许你返回从 async
获得的 future,以便调用链中更靠上的代码可以附加它们自己的处理程序。
你还可以使用相同的流畅风格为 Future
指定超时。 如果指定的超时在闭包完成之前已经过去,则会在 Future
中设置一个错误,并调用其 .onFailure
和 .onCompletion
处理程序。 有关更多信息,请参见 Future
的注释文档。
作为上述流畅的、函数式风格的替代方法,你可以以更传统的命令式方式使用 Future
,作为尚未确定的值的占位符。 以这种方式使用,它更像 C++ 的 std::future
。 当你使用 async
将较大的任务细分为多个并发子任务时,这尤其有用,这些子任务必须组合成最终结果才能继续。
当使用 Future
作为占位符时,您可以将其存储起来,就像存储异步代码返回的实际值或抛出的错误一样(如果在同步调用时)。 然后,当您需要时,可以查询 Future
以获取值或错误。 为了支持这一点,Future
提供了阻塞属性和方法来查询 Future 并等待其准备就绪,以及一个非阻塞属性来查询其准备状态。
无论您是否将 Future
用作占位符,附加的所有处理程序都将仍然运行。 这两种使用方式可以结合使用。
请注意,在 AppKit/UIKit 应用程序中,在主线程中使用阻塞方法和属性可能会使您的应用程序在阻塞期间无响应。 您可以在可以安全阻塞的单独线程中使用它们,可以使用 .isReady
在 Future
未准备好时执行其他操作,确保所有异步调用都将快速完成,或者直接避免使用阻塞方法和属性,而是附加处理程序。
您可以使用 .value
和 .error
属性从 Future 中获取值或错误。 我们将使用与前面示例中相同的 foo
。
let future = async { return try foo() }
// future.value and future.error will block until the foo returns or throws
if let value = future.value {
print("foo returned \(value)")
}
else if let error = future.error {
print("foo threw exception, \(error.localizedDescription)")
}
这些属性仅在 Future 准备就绪时返回,这意味着 foo
要么已返回一个值,要么已抛出一个错误。 在此之前,它们会阻塞,等待 foo
完成。 当 foo
完成时,以下情况之一将为真:
.value
将包含该值,而 .error
将为 nil
。.value
将为 nil
,而 .error
将包含该错误。Future
永远不会同时具有错误和值。
请注意,如果如上所述设置了 timeout
修饰符,并且在闭包完成之前经过了指定的超时时间,则 .error
将包含 FutureError.timedOut
。
如果您更喜欢使用 Swift 的 Result
类型,则可以访问 .result
属性,而不是 .value
和 .error
。
let future = async { return try foo() }
// future.result will block until future is ready
switch future.result {
case let .success(value):
print("foo returned \(value)")
case let .failure(error):
print("foo threw exception, \(error.localizedDescription)")
}
.result
的阻塞方式与 .value
和 .error
完全相同。
如果如上所述设置了 timeout
修饰符,并且在闭包完成之前经过了指定的超时时间,则 .result
将为 .failure(FutureError.timedOut)
。
如果您更喜欢使用 do {...} catch {...}
块进行错误处理,则 Future
提供了一个抛出异常的 .getValue()
方法。
let future = async { return try foo() }
// future.getValue() will block until the foo returns or throws
do
{
let value = try future.getValue()
print("foo returned \(value)")
}
catch { print("foo threw exception, \(error.localizedDescription)") }
如果您不需要实际结果(可能您的闭包返回 Void
),您可以简单地调用 .wait()
方法。
let future = async { let _ = try foo() }
future.wait() // block until future is ready
// Now the future is ready, so do other stuff
.wait()
也有一个超时变体。 它与上面提到的 timeout
修饰符不同,因为它不会在超时时在 Future
中设置错误。 它只是停止等待 Future
准备就绪,而是在超时时抛出自己的错误。 有关更多信息,请参阅 Future
的注释文档。
如果跟在后面的代码依赖于 foo
返回的值或抛出的错误,则 .wait()
、.value
、.error
和 .result
的阻塞行为很有用,但如果您可以在等待 Future
准备就绪时执行其他工作,则这可能会成为一个问题,因为它会停止您当前的线程,直到 foo
完成,在这种情况下,您从使用 async
中获得的价值并不多。 因此,无需阻塞即可确定 Future
是否已准备就绪的能力至关重要。 您可以使用 .isReady
属性来执行此操作。
let future = async { return try foo() }
while !future.isReady {
// Do some other work while we wait
}
if let value = future.value {
print("foo returned \(value)")
}
else if let error = future.error {
print("foo threw exception, \(error.localizedDescription)")
}
如果您不打算在等待 Future
准备就绪时执行其他工作,那么调用 .wait()
比循环调用 .isReady
效率更高,因为 Future
的所有阻塞方法和属性(包括 .wait()
)都在底层使用 DispatchSemaphore
,它可以真正挂起线程,而旋转 .isReady
会不必要地消耗 CPU 周期。
所有的 async
变体都允许您指定服务质量 (qos) 和标志,就像 GCD 原生的 async
和 asyncAfter
一样,并在您未指定它们时使用相同的默认值。
以下示例使用全局 async 自由函数,但 DispatchQueue
扩展提供了等效的实例方法,其签名与全局函数相同,因此您可以在特定的 DispatchQueue
上调用它们。
如果您希望您的闭包尽快执行,您可以像前面的示例一样调用它,而无需截止日期或延迟间隔。
let future = async { return try foo() }
如果您需要延迟闭包的执行,直到特定的时间点,您可以使用 afterDeadline
变体来指定 DispatchTime
或 Date
。
let deadline: DispatchTime = .now() + .milliseconds(1000)
// Some time later...
let future = async(afterDeadline: deadline) {
return try foo()
}
要仅使用 DispatchTimeInterval
指定延迟间隔,您可以使用 afterInterval
变体。
let future = async(afterInterval: .milliseconds(1000)) {
return try foo()
}
或者,您可以使用 afterSeconds
变体以秒为单位指定延迟作为 TimeInterval
。
let future = async(afterSeconds: 1) { return try foo() }
sync
是 async
的同步对偶。 它在返回之前在当前线程中运行传递给它的闭包。
如果未指定截止日期或延迟,它将立即执行闭包。 如果指定了截止日期或延迟,它将阻塞直到截止日期或延迟过去,然后执行闭包。 因为 sync
在闭包执行完毕后才会返回,所以如果它们适用,则附加到它返回的 Future
的任何处理程序将在它们附加后立即执行。
否则,对于 async
所说的一切都适用于 sync
。
并发代码(例如由 async
执行的代码)的一个不幸的副作用是,可以由多个任务同时访问的任何可变共享数据必须受到保护,以避免数据竞争。 这就是 Mutex
的用途。 您创建一个 Mutex
来保护某些数据,然后在访问共享数据之前锁定它,并在之后解锁它。 显式地锁定和解锁 Mutex
容易出错,因此使用此 Mutex
实现的首选方式是通过其 withLock
方法。 例如,这是一个 SharedStack
的简单实现
class SharedStack<T>
{
private var data = [T]()
private var mutex = Mutex()
public var isEmpty: Bool {
return mutex.withLock { data.isEmpty }
}
public init() {}
public func push(_ value: T) {
mutex.withLock { data.append(value) }
}
public func pop() -> T? {
return mutex.withLock {
return data.isEmpty ? nil : data.removeLast()
}
}
}
withLock
阻塞直到可以获得锁,一旦获得锁,它就会执行传递给它的闭包,并在返回之前解锁。 解锁发生在闭包完成之后立即进行,无论它是返回还是抛出。
Mutex
还提供了一个可失败的 withAttemptedLock
方法,允许您指定一个超时时间(可能没有),超过该时间它将停止等待锁并抛出 MutexError.lockFailed
。 如果失败,则不会获得锁并且不会运行闭包。
尽管显式锁定和解锁 Mutex
容易出错,但在某些情况下,withLock
和 withAttemptedLock
都无法完成这项工作,例如以既不相互排斥也不干净地可嵌套的方式交错锁定和解锁多个互斥锁。 对于这些情况,Mutex
还提供了 lock()
、tryLock()
和 unlock()
方法。 当你可以使用它们时,优先使用 withLock
和 withAttemptedLock
。 如果您必须自己显式锁定和解锁互斥锁,您有责任确保每个 lock()
或成功的 tryLock()
都由一个且只有一个 unlock()
平衡,否则,您将死锁,或者在 Mutex
被取消初始化时崩溃。 这种崩溃行为是由它以 DispatchSemaphore
实现的方式继承的,当以负值取消初始化时,它会崩溃,如果 Mutex
仍然被锁定,它将具有负值。 这实际上是一件好事,因为它明确地告诉您您有一个错误。 引用 Apple 关于此主题的文档:不要那样做!
有关这些其他方法的更多信息,请参阅注释文档。
Promise
是 Promise
/Future
团队的发送者。 这是您如何从您自己的代码中获得要返回的 Future
的方法,以及如何从可能远离接收 Future
的代码执行的代码中设置 Future
中的值,可能在完全不同的线程中。
如果您希望在您自己的自定义代码中返回 Future
,您可以通过创建一个 Promise
并在立即上下文中返回其 .future
属性来实现,同时将返回值的闭包(可能抛出一个错误)传递给分派上下文中的 .set(from:)
方法。
例如,假设您想包装 URLSession
的 .dataTask
以将 Future
返回到生成的 Data
。
extension URLSession
{
func dataFuture(with url: URL) -> Future<Data>
{
let promise = Promise<Data>()
let task = self.dataTask(with: url) {
(data, response, error) in
// ignoring response for this example
promise.set {
if let error = error {
throw error
}
return data ?? Data()
}
}
task.resume()
return promise.future
}
}
.set(from:)
方法将根据闭包是否返回或抛出异常来设置 Future
,在本例中,这取决于 dataTask
是否使用错误调用其完成处理程序。
.dataTask
使用非 nil
错误调用其完成处理程序,则传递给 .set(from:)
的闭包将抛出异常,导致 Promise
设置 Future
的 .error
。.dataTask
使用 nil
错误调用其完成处理程序,则传递给 .set(from:)
的闭包将返回 data
,导致 Promise
将 Future
的 value
设置为 data
。在此示例中,我们忽略 response
,并且由于我们返回 Future
而不是 URLSessionDataTask
,因此我们还在返回之前恢复从 dataTask
返回的任务。
Promise
还提供了一个 setResult(from:)
方法,该方法接受一个返回 Swift Result
的非抛出闭包。
extension URLSession
{
func dataFuture(with url: URL) -> Future<Data>
{
let promise = Promise<Data>()
let task = self.dataTask(with: url) {
(data, response, error) in
// ignoring response for this example
promise.setResult { () -> Result<Data, Error> in
if let error = error {
return .failure(error)
}
return .success(data ?? Data())
}
}
task.resume()
return promise.future
}
}