异步 (Async)

Async 是一个免费使用的函数和类型框架,我发现在使用Grand Central Dispatch时非常有用。尽管该库的核心是 Future 及其对应的 Promise,但它们在更广泛的上下文中也很有用。 我长期以来一直在我的代码中使用它们,并且一直想分享它们,所以现在我终于这么做了。 我还有其他发现有用的类型和函数,我可能会在以后添加,但这些构成了我几乎每次使用 GCD 时使用的核心功能。

即使 Future 才是真正的核心特性,该库还是被称为 Async,因为它提供了全局的 async 自由函数,并在 DispatchQueue 上添加了相应的返回 Future 的方法,因此我几乎不需要显式地创建 Promise,而且通常 Future 本身会消失在流畅的完成处理程序语法之后,以至于看起来该库似乎是关于 async 的。但实际上,Future 才是英雄,它比我见过的任何 Future 实现都更灵活。

我经常尝试使用命令行工具而不是实际的 AppKit/UIKit 应用程序来实现一个想法,因此该软件包专门设计为独立于这些。它仅使用 GCD 和 Foundation。

包含内容

async

全局 async 自由函数有几种变体,它们会立即返回一个 Future,异步执行你的闭包,该闭包可以安全地抛出错误,在全局默认并发 DispatchQueue 上执行。 你无需显式地创建队列,除非你需要它用于其他原因。

我提供了几种变体,因为我发现必须在 GCD 的 asyncAfter 中将截止日期指定为 DispatchTime 很烦人。 我几乎总是希望我的代码尽快运行,或者在调用 async 后的指定延迟后运行,几乎从不需要在特定时间点运行它。 因此,我提供了 async 函数的变体,允许你像 GCD 的原生版本一样将截止日期指定为 DispatchTime,或者将延迟指定为 DispatchTimeIntervalTimeInterval

sync

有全局 async 自由函数的同步版本。 它们只是立即在当前线程中运行你的闭包,就像你直接调用它一样。 这对于以下几个原因很有用

• 它允许在将同步代码重构为异步代码的过程中进行中间步骤,在此期间你仍然以同步方式执行闭包,同时从中获取 Future。 在这种情况下,当你的闭包返回时,Future 会立即准备就绪,如果你添加了处理程序,它们会立即被调用。 然后,只需将 sync 更改为 async,它就会变成真正的异步。

• 有时使用 sync 代替 async 进行调试会很有帮助,而且这是一个简单的一字母代码更改。

• 它返回一个 Future,因此你可以在自己的代码中使用它,作为一种简单的方式来返回 Future,而无需担心如何使用 Promise,尽管 Promise 也很容易直接使用。

DispatchQueue 扩展

asyncsync 方法已添加到 DispatchQueue 以对应于它们的全局版本,但是你可以在你选择的队列上获得 Future 返回行为,而不是默认的全局并发队列。

互斥锁 (Mutex)

Mutex 是一个类,它通过提供 withLock 方法来简化对共享数据进行线程安全的保护,以自动锁定和解锁互斥锁,这应该是最常用的方法。 但是,认识到有时需要以并非总是可以方便地嵌套的方式交错锁定和解锁不同的互斥锁,它还提供了显式的 lockunlock 方法以及一个可失败的 tryLock 方法,允许你指定超时。

Future

此实现允许你使用在大多数库中看到的典型流畅风格,但它也允许你将 Future 用作值的真正占位符,类似于 C++ 的 std::future,你可以将其存储起来并在需要时查询。 它还允许你为这两种用法指定超时。

Promise

PromiseFuture 的必要但通常隐藏的对应物。 即使它的接口非常简单,如果你主要只需要从 async 获取 Future,你永远不需要自己创建 Promise。 另一方面,你可以使用 Promise 从你喜欢的任何代码中返回 Future,例如你自己的第三方异步库包装器。

基本用法

尽管每个函数和类型都已使用标记注释进行了完整记录,因此在 Xcode 中,你可以通过 QuickLook 轻松获得参考,但是看到至少基本的示例作为起点是有帮助的。 我将按照我认为大多数人使用它们的顺序来介绍这些示例。

Future 和 async

Future 孤立地看没有多大意义,因此我将结合 async 来描述它,但要理解从根本上讲,直接获得 Future 的唯一方法是从 Promise,稍后将对此进行描述。 async 在内部使用 Promise 来返回 Future。 这没有什么特别之处,你可以自己这样做以在其他上下文中使用 Future

你可以通过几种方式来考虑 Future,而且它们都是同时成立的

将处理程序(回调)附加到 Future 是防止你的代码阻塞结果的最简单和最安全的方法,因为你可以在附加它们后继续做其他事情,并且你的处理程序会在你的异步代码完成时自动调用。 就易用性和安全性而言,处理程序通常是首选方法。

但是,它们确实有一些缺点。 如果你要合并必须全部完成才能取得进展的多个异步任务的结果,处理程序可能会很尴尬。 例如,在计算图中,所有输入计算必须在当前节点可以评估之前完成。 在这种情况下,当前计算应阻塞,直到输入准备就绪。 当然,你可以将处理程序的方钉强行塞进这个圆孔中,但这不是处理程序的好用例。 你需要更新输入的一些计数器,确保从多个处理程序对该计数器的更新是原子性的,并且阻塞该计数器。 这并不难,但是多么不必要、低效且容易出错的混乱! 相比之下,将 Future 的集合用作占位符是理想的选择。 只需迭代地等待它们中的每一个。 当循环完成时,它们都已准备就绪,你可以继续评估当前节点。 这正是我提供替代方案的原因。

另一方面,从网络获取数据或响应事件,这是非常常见的情况,通常是处理程序的理想用途,而保留 Future 作为占位符会变得很尴尬,因为它需要你阻塞直到它准备就绪,或者编写某种循环或其他方案来持续检查它何时准备就绪,而你的代码继续执行。

话虽如此,完成处理程序非常适合回调,例如对于 HTTP 请求,将它们用作占位符是避免深度嵌套回调的好方法,当一个请求的结果需要生成另一个请求时,以此类推。

此软件包中 async 的实现与 GCD 的原生 asyncasyncAfter 方法在两个方面有所不同。 第一个是它返回一个 Future,第二个是除了 DispatchQueue 本身的方法之外,还有使用默认并发 DispatchQueue 的全局自由函数变体。

当你调用 async 时,它会安排你的闭包执行,使用 GCD 的原生 async,但它也会立即返回一个 Future,用于你的闭包将返回的值,或者它将抛出的错误。 你可以保留此 Future 作为查询你的闭包最终结果的一种方式,或者你可以使用它来附加处理程序...或者两者兼而有之。 这两种使用方式可以一起使用,如果这对你的应用程序有意义的话。

Future 处理程序附加

.onSuccess.onFailure

作为一个基本示例,假设我们有一个长时间运行的函数 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)")
}

你可以按任何顺序附加处理程序,如果你愿意先放置失败处理程序。

.onCompletion

如果你更喜欢使用 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,以便调用链中更靠上的代码可以附加它们自己的处理程序。

.timeout

你还可以使用相同的流畅风格为 Future 指定超时。 如果指定的超时在闭包完成之前已经过去,则会在 Future 中设置一个错误,并调用其 .onFailure.onCompletion 处理程序。 有关更多信息,请参见 Future 的注释文档。

Future 作为占位符

作为上述流畅的、函数式风格的替代方法,你可以以更传统的命令式方式使用 Future,作为尚未确定的值的占位符。 以这种方式使用,它更像 C++ 的 std::future。 当你使用 async 将较大的任务细分为多个并发子任务时,这尤其有用,这些子任务必须组合成最终结果才能继续。

当使用 Future 作为占位符时,您可以将其存储起来,就像存储异步代码返回的实际值或抛出的错误一样(如果在同步调用时)。 然后,当您需要时,可以查询 Future 以获取值或错误。 为了支持这一点,Future 提供了阻塞属性和方法来查询 Future 并等待其准备就绪,以及一个非阻塞属性来查询其准备状态。

无论您是否将 Future 用作占位符,附加的所有处理程序都将仍然运行。 这两种使用方式可以结合使用。

阻塞方法和属性

请注意,在 AppKit/UIKit 应用程序中,在主线程中使用阻塞方法和属性可能会使您的应用程序在阻塞期间无响应。 您可以在可以安全阻塞的单独线程中使用它们,可以使用 .isReadyFuture 未准备好时执行其他操作,确保所有异步调用都将快速完成,或者直接避免使用阻塞方法和属性,而是附加处理程序。

.value.error

您可以使用 .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 完成时,以下情况之一将为真:

Future 永远不会同时具有错误和值。

请注意,如果如上所述设置了 timeout 修饰符,并且在闭包完成之前经过了指定的超时时间,则 .error 将包含 FutureError.timedOut

.result

如果您更喜欢使用 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)

.getValue()

如果您更喜欢使用 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)") }
.wait()

如果您不需要实际结果(可能您的闭包返回 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 的注释文档。

非阻塞方法和属性
.isReady

如果跟在后面的代码依赖于 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 变体

所有的 async 变体都允许您指定服务质量 (qos) 和标志,就像 GCD 原生的 asyncasyncAfter 一样,并在您未指定它们时使用相同的默认值。

以下示例使用全局 async 自由函数,但 DispatchQueue 扩展提供了等效的实例方法,其签名与全局函数相同,因此您可以在特定的 DispatchQueue 上调用它们。

.async()

如果您希望您的闭包尽快执行,您可以像前面的示例一样调用它,而无需截止日期或延迟间隔。

let future = async { return try foo() }
.async(afterDeadline:)

如果您需要延迟闭包的执行,直到特定的时间点,您可以使用 afterDeadline 变体来指定 DispatchTimeDate

let deadline: DispatchTime = .now() + .milliseconds(1000)

// Some time later...
let future = async(afterDeadline: deadline) {
    return try foo()
}
.async(afterInterval:)

要仅使用 DispatchTimeInterval 指定延迟间隔,您可以使用 afterInterval 变体。

let future = async(afterInterval: .milliseconds(1000)) {
    return try foo()
}
.async(afterSeconds:)

或者,您可以使用 afterSeconds 变体以秒为单位指定延迟作为 TimeInterval

let future = async(afterSeconds: 1) { return try foo() }

sync

syncasync 的同步对偶。 它在返回之前在当前线程中运行传递给它的闭包。

如果未指定截止日期或延迟,它将立即执行闭包。 如果指定了截止日期或延迟,它将阻塞直到截止日期或延迟过去,然后执行闭包。 因为 sync 在闭包执行完毕后才会返回,所以如果它们适用,则附加到它返回的 Future 的任何处理程序将在它们附加后立即执行。

否则,对于 async 所说的一切都适用于 sync

互斥锁 (Mutex)

并发代码(例如由 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 容易出错,但在某些情况下,withLockwithAttemptedLock 都无法完成这项工作,例如以既不相互排斥也不干净地可嵌套的方式交错锁定和解锁多个互斥锁。 对于这些情况,Mutex 还提供了 lock()tryLock()unlock() 方法。 当你可以使用它们时,优先使用 withLockwithAttemptedLock如果您必须自己显式锁定和解锁互斥锁,您有责任确保每个 lock() 或成功的 tryLock() 都由一个且只有一个 unlock() 平衡,否则,您将死锁,或者在 Mutex 被取消初始化时崩溃。 这种崩溃行为是由它以 DispatchSemaphore 实现的方式继承的,当以负值取消初始化时,它会崩溃,如果 Mutex 仍然被锁定,它将具有负值。 这实际上是一件好事,因为它明确地告诉您您有一个错误。 引用 Apple 关于此主题的文档:不要那样做!

有关这些其他方法的更多信息,请参阅注释文档。

Promise

PromisePromise/Future 团队的发送者。 这是您如何从您自己的代码中获得要返回的 Future 的方法,以及如何从可能远离接收 Future 的代码执行的代码中设置 Future 中的值,可能在完全不同的线程中。

.set(from:)

如果您希望在您自己的自定义代码中返回 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 是否使用错误调用其完成处理程序。

在此示例中,我们忽略 response,并且由于我们返回 Future 而不是 URLSessionDataTask,因此我们还在返回之前恢复从 dataTask 返回的任务。

.setResult(from:)

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
    }
}