MiniFuture 是一个在 Swift 语言中实现的单子 Future 设计模式,它使用了 libdispatch 和 POSIX 互斥锁与条件变量。其设计灵感来源于 Scala 的 scala.concurrent.Future。
目前仅实现了基本的核心功能。我们已经在生产环境中使用该库,并且它运行正常。有一个基准测试可以作为压力测试,请参阅下面的 性能 部分。
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
。