MiniFuture

MiniFuture 是一个在 Swift 语言中实现的单子 Future 设计模式,它使用了 libdispatch 和 POSIX 互斥锁与条件变量。其设计灵感来源于 Scala 的 scala.concurrent.Future

目前仅实现了基本的核心功能。我们已经在生产环境中使用该库,并且它运行正常。有一个基准测试可以作为压力测试,请参阅下面的 性能 部分。

Pod version Build status

要求

安装

CocoaPods

CocoaPods 是 Cocoa 库的集中式依赖管理器。它将库作为嵌入式框架集成到你的项目中。这要求你项目的最低部署目标为 iOS 8.0 或 OS X 10.9。

MiniFuture 可以通过 pod 方式获取。将以下行添加到你项目的 Podfile 文件中,即可将该库作为依赖项引入

pod 'MiniFuture', '~> 0.5.0'

并运行 pod install

在你的源代码中,import MiniFuture 以使用该库。

手动

Source 目录下的所有文件复制到你的项目中。你需要自行解决如何升级库的问题。

特性

Future 代表一个最终会完成的计算的承诺。我们使用 Try<T> 值类型来包装这些计算结果。它是一个枚举,包含两个成员:success<T>failure<Error>。第一个成员用于 Future 的调用者表示计算成功,其中计算结果作为类型为 T 的关联值。后一个成员表示计算失败,其中 Error 作为关联值。

例如,以下 future 最终将以 .success(1) 完成

let fut = Future<Int>.async {
  Thread.sleep(forTimeInterval: 0.2)
  return .success(1)
}

assert(succeeded.get() == Try.success(1))

组合 Futures 的基本操作是使用 Future#flatMap(_:)。该方法的签名是

flatMap<U>(f: T throws -> Future<U>) -> Future<U>

它接受一个闭包 f 作为参数。如果当前的 future 以成功值完成,则会调用 f,并将 T 作为其参数。f 的返回值是下一个 future 计算。稍后,如果该 future 以成功值完成,则会调用传递给该 future 的闭包。这就是 futures 的异步调用链。

如果当前的 future 以失败值完成,则不会调用 f。这会短路 futures 的调用链。

当调用闭包 f 时,可能会抛出错误。如果发生这种情况,f 的返回值将是一个以失败值完成的 future,其中包含该错误。

例如

enum AppError: Error {
  case deliberate(String)
}

let fut: Future<[Int]> = Future.async { .success(0) }
  .flatMap { Future.succeeded([$0, 1]) }
  .flatMap { throw AppError.deliberate(String(describing: $0)) }
  .flatMap { Future.succeeded($0 + [2]) }

assert(String(describing: fut.get()) == "Try.failure(deliberate(\"[0, 1]\"))")

Try<T> 和对抛出错误的自动处理借鉴自 Scala 2.10。

所有异步操作都在 libdispatch 的默认全局并发队列中运行。传递给 Future#flatMap(_:)Future#map(_:)Future#onComplete(_:)Future.async(_:) 的闭包始终在队列工作线程中执行。当通过闭包中捕获的引用访问共享状态时,请使用适当的同步机制。

用法

要启动一个 Future 任务,请使用 Future.succeeded(_:)Future.failed(_:) 来包装立即值。这些方法返回 ImmediateFuture 对象,这是一种 Future 实现类,它已经以成功或失败值完成。

对于稍后在队列工作线程中计算值的异步任务,请使用 Future.async(_:)。你向 async(_:) 传递一个代码块,并从中返回 Try.success<T>Try.failure<Error> 值。此处的 Future 实现类是 AsyncFuture

为了使用 Futures 适配现有的异步接口,请使用 Future.promise(_:)。这会返回一个 PromiseFuture 对象,这是一种 promise 类型的 Future 实现类。将 Future 传递给现有的异步接口,并在接口的完成处理程序中,使用成功 (Future#resolve(_:)) 或失败 (Future#reject(_:)) 完成 Future。你可以立即将 PromiseFuture 返回给期望 Futures 的代码,并让 PromiseFuture 对象稍后完成。

你也可以使用另一个 future 的结果来完成 PromiseFuture。调用 PromiseFuture#completeWith(_:),并将另一个 future 作为参数传递。一旦该 future 完成,promise 将以与该 future 相同的结果完成。

当你获得一个 Future 的句柄时,请使用 Future#flatMap(_:)Future#map(_:) 来组合另一个依赖于前一个 Future 完成结果的 Future。使用 Future#get() 来等待 Future 的结果。使用 Future#onComplete(_:) 来添加一个回调,以便在 Future 完成时运行副作用。

示例

extension String {
  var trimmed: String {
    return trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
  }

  func excerpt(_ maxLength: Int) -> String {
    precondition(maxLength >= 0, "maxLength must be positive")

    if maxLength == 0 {
      return ""
    }

    let length = characters.count

    if length <= maxLength {
      return self
    }

    return self[startIndex..<characters.index(startIndex, offsetBy: maxLength-1)].trimmed + ""
  }
}

/**
 * Request a web resource asynchronously, immediately returning a handle to
 * the job as a promise kind of Future. When NSURLSession calls the completion
 * handler, we fullfill the promise. If the completion handler gets called
 * with the contents of the web resource, we resolve the promise with the
 * contents (the success case). Otherwise, we reject the promise with failure.
 */
func loadURL(_ url: URL) -> Future<Data> {
  let promise = Future<Data>.promise()
  let task = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
    if let err = error {
      promise.reject(err)
    } else if let d = data {
      promise.resolve(d)
    } else {
      promise.reject(AppError.failedLoadingURL(url))
    }
  })
  task.resume()
  return promise
}

/**
 * Parse data as HTML document, finding specific contents from it with an
 * XPath query. We return a completed Future as the handle to the result. If
 * we can parse the data as an HTML document and the query succeeds, we return
 * a successful Future with the query result. Otherwise, we return failed
 * Future describing the error.
 *
 * Because this function gets called inside `Future#flatMap`, it's run in
 * background in a queue worker thread.
 */
func readXPathFromHTML(_ xpath: String, data: Data) throws -> Future<HTMLNode> {
  let doc = try HTMLDocument.readData(asUTF8: data)
  let node = try doc.rootHTMLNode()
  let found = try node.forXPath(xpath)
  return Future.succeeded(found)
}

let wikipediaURL = URL(string: "https://en.wikipedia.org/wiki/Main_Page")!
let featuredArticleXPath = "//*[@id='mp-tfa']"

let result = loadURL(wikipediaURL)
  /* Future composition (chaining): when this Future completes successfully,
   * pass its result to a function that does more work, returning another
   * Future. If this Future completes with failure, the chain short-circuits
   * and further flatMap methods are not called. Calls to flatMap are always
   * executed in a queue worker thread.
   */
  .flatMap { try readXPathFromHTML(featuredArticleXPath, data: $0) }
  /* Wait for Future chain to complete. This acts as a synchronization point.
   */
  .get()

switch result {
case .success(let value):
  let excerpt = value.textContents!.excerpt(78)
  print("Excerpt from today's featured article at Wikipedia: \(excerpt)")
case .failure(let error):
  print("Error getting today's featured article from Wikipedia: \(error)")
}

更多示例请参见 Example/main.swift。你可以运行这些示例

$ make example
# xcodebuild output…

./build/Example
Excerpt from today's featured article at Wikipedia: Upper and Lower Table Rock are two prominent volcanic plateaus just north of…

性能

Benchmark/main.swift 中有一个基准测试。它在一个循环中构建复杂的嵌套 Futures(代码中的 futEnd 变量)2000 次 (numberOfFutureCompositions),并将它们链接成一个大的复合 Future(fut 变量)。然后,基准测试等待 Future 完成。

我们重复此过程 500 次 (numberOfIterations) 以获得完成每个复合 Future 所花费时间的算术平均值和标准差。

使用 Release 构建配置编译它,这将启用 -O 编译器标志。然后从终端运行它。

在 MacBook Pro (MacBookPro11,3) 2.6 GHz Intel Core i7 Haswell, 16 GB 1600 MHz DDR3 上运行的示例

$ make benchmark
# xcodebuild output…

./build/Benchmark
iterations: 500, futures composed: 2000

warm up: 71 ms (± 2 ms)
measure: 71 ms (± 2 ms)

Apple Swift version 4.0.3 (swiftlang-900.0.74.1 clang-900.0.39.2)
Target: x86_64-apple-macosx10.9

进程的总内存消耗保持在 20 MB 以下。

未来工作

许可证

MiniFuture 在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE.txt