Fork me on GitHub

源码解读——PromiseKit

PromiseKit 是一款基于 Swift 的 Promise 异步编程框架,作者是大名鼎鼎的 Max Howell,Max Howell 同时也是 Homebrew 的作者,因在面试 Google 时写不出算法题反转二叉树而走红。

最近,我在研究 Promise 异步编程,一开始尝试从阅读 PromiseKit 源码上手,但是发现里面的一些设计理念难以理解。因此,转而去研究 Promise 核心原理,并产出了一篇文章——《Promise 核心实现原理》。最后,再回过头来阅读 PromiseKit 源码,很多当初不理解的设计立马豁然开朗了。这里,希望通过本文记录自己对于 PromiseKit 设计思想的一些理解。

注:本文分析的 PromiseKit 版本是 6.18.1

Thenable & CatchMixin

Thenable 是 PromiseKit 的核心协议之一,其声明了一个关键方法 pipe(to:),并实现了一系列链式操作符(方法),具体如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public protocol Thenable: AnyObject {
/// The type of the wrapped value
associatedtype T

/// `pipe` is immediately executed when this `Thenable` is resolved
func pipe(to: @escaping(Result<T>) -> Void)

/// The resolved result or nil if pending.
var result: Result<T>? { get }
}

public extension Thenable {
func then<U: Thenable>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) throws -> U) -> Promise<U.T>

func map<U>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T) throws -> U) -> Promise<U>

func compactMap<U>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T) throws -> U?) -> Promise<U>

func done(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) throws -> Void) -> Promise<Void>

func get(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping (T) throws -> Void) -> Promise<T>

func tap(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(Result<T>) -> Void) -> Promise<T>

func asVoid() -> Promise<Void>

...
}

遵循 Thenable 协议的有两个泛型类型,分别是:

  • Promise<T>:支持异步任务的成功和失败状态。
  • Guarantee<T>:仅支持异步任务的成功状态,不接受失败状态。

两者的主要区别在于 Promise 支持失败状态,而 Guarantee 不支持失败状态。因此,PromiseKit 定义了另一个协议 CatchMixin,该协议声明并实现了错误处理相关的方法,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public protocol CatchMixin: Thenable {}

public extension CatchMixin {
func `catch`(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) -> Void) -> PMKFinalizer

func recover<U: Thenable>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> U) -> Promise<T> where U.T == T

func recover(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(Error) -> Guarantee<T>) -> Guarantee<T>

func ensure(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping () -> Void) -> Promise<T>

func ensureThen(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping () -> Guarantee<Void>) -> Promise<T>

func recover(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(Error) -> Void) -> Guarantee<Void>

func recover(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> Void) -> Promise<Void>

...
}

CatchMixin 协议继承自 Thenable 协议,从而限制 PromiseKit 只为遵循 Thenable 协议的类型支持 catchrecover 等能力。

Promise

《Promise 核心实现原理》 中,我们知道 Promise 内部主要包含几个部分:

  • 执行状态:用于表示 Promise 的三种状态,pendingfulfilledrejected
  • 执行结果:对于成功状态,为任务的返回值;对于失败状态,为任务的错误码。
  • 回调任务列表:当 Promise 达到结束态时,将自动执行回调任务列表中的任务,并将当前执行结果作为参数传入回调任务。
  • 执行器:用于更新执行状态和执行结果,并执行回调任务列表中的任务。

在 PromiseKit 的实现中,使用两个类型来表示 4 个部分:

  • Box:表示一个容器,包含了 执行状态执行结果回调任务列表
  • Resolver执行器

《Promise 核心实现原理》 中,我们设计实现的 Promise 强关联了回调任务列表和执行器,执行器同时反向强引用了 Promise。然而,在 PromiseKit 中,Promise 仅强关联了 Box,而弱依赖了 ResolverResolver 则强关联了 Box。当然,两种设计在内存管理中的作用是一样的。如下所示为 PromiseBoxResolver 的引用关系。

下面,我们来分别介绍一下 BoxResolver 的设计。

Box

PromiseKit 通过两种具体的 Box 子类来表示不同状态下的容器,分别是:

  • EmptyBox:表示 pending 状态下的容器。
  • SealedBox:表示 resolved 状态下的容器,具体可以是 fulfilledrejected 状态。

执行状态 & 执行结果 & 回调任务列表

Promise 通过 Sealant 枚举类型将三种执行状态(pendingfulfilledrejected)分成两种执行状态:

  • 开始状态pending 状态。
  • 结束状态resolved 状态,具体可以是 fulfilledrejected

Sealant 的定义如下所示:

1
2
3
4
5
6
7
8
9
enum Sealant<R> {
case pending(Handlers<R>)
case resolved(R)
}

final class Handlers<R> {
var bodies: [(R) -> Void] = []
func append(_ item: @escaping(R) -> Void) { bodies.append(item) }
}

其中,pending 状态的关联值存储了 回调任务列表 Handlersresolved 状态的关联值存储了两种细分的状态 Result

Result 枚举类型则用于进一步表示 fulfilledrejected 状态,具体定义如下所示:

1
2
3
4
public enum Result<T> {
case fulfilled(T)
case rejected(Error)
}

通过关联对象,分别存储两种 执行结果:返回值 T 和错误码 Error

EmptyBox & SealedBox

Box 抽象类定义了三个方法,分别是:

1
2
3
4
5
class Box<T> {
func inspect() -> Sealant<T> { fatalError() }
func inspect(_: (Sealant<T>) -> Void) { fatalError() }
func seal(_: T) {}
}

inspect() 方法用于检查内部状态,返回值为 Sealant 值。对于 SealedBox,其返回始终为 resolved<T>

inspect(_ body: (Sealant<T>) -> Void) 方法将内部结果作为参数传递给闭包并执行。

seal(_: T) 方法非常关键,当 Boxpending 状态时,seal 方法可以将内部状态更新为 resolved,同时执行 回调任务列表 中的所有任务。Resolver 就是通过 seal 方法来更新状态的。具体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class EmptyBox<T>: Box<T> {
...

override func seal(_ value: T) {
var handlers: Handlers<T>!
barrier.sync(flags: .barrier) {
guard case .pending(let _handlers) = self.sealant else {
return // already fulfilled!
}
handlers = _handlers
self.sealant = .resolved(value)
}

if let handlers = handlers {
handlers.bodies.forEach{ $0(value) }
}
}
}

Resolver

Resolver 的核心作用是更新执行状态和执行结果,并执行回调任务列表中的任务。由于 Box 封装了执行状态、执行结果、回调任务列表,并且提供了更新状态的方法 seal。因此,Resolver 只需提供针对不同状态的便利方法,内部调用 Boxseal 方法进行更新即可。具体如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public final class Resolver<T> {
let box: Box<Result<T>>

init(_ box: Box<Result<T>>) {
self.box = box
}

deinit {
if case .pending = box.inspect() {
conf.logHandler(.pendingPromiseDeallocated)
}
}
}

public extension Resolver {
/// Fulfills the promise with the provided value
func fulfill(_ value: T) {
box.seal(.fulfilled(value))
}

/// Rejects the promise with the provided error
func reject(_ error: Error) {
box.seal(.rejected(error))
}

/// Resolves the promise with the provided result
func resolve(_ result: Result<T>) {
box.seal(result)
}
...
}

任务串联

Promise 的核心能力之一是串联任务(异步或同步)。从 Thenable 的方法中,我们可以看到几乎所有的串联方法内部都是通过 pipe(to:) 方法实现任务串联的。下面,我们来看一下 Promise 中该方法的定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final class Promise<T>: Thenable, CatchMixin {
...
public func pipe(to: @escaping(Result<T>) -> Void) {
switch box.inspect() {
case .pending:
box.inspect {
switch $0 {
case .pending(let handlers):
handlers.append(to)
case .resolved(let value):
to(value)
}
}
case .resolved(let value):
to(value)
}
}
}

从实现中可以看到,pipe(to:) 方法会先判断 Box 的状态,如果是 pending 状态,则将闭包加入回调任务列表;如果是 resolved 状态,则立即执行闭包。

内存管理

PromiseKit 所实现的 Promise 的内存管理是非常清晰。我们可以通过阅读各种链式操作符的内部实现来梳理其内存引用关系。如下所示,为链式操作符下产生的线性内存引用关系。

当对 promise 0 执行链式操作符时,链式操作符会创建一个 promise 1,并在内部创建一个匿名闭包对用户闭包进行封装。同时,匿名闭包将引用 promise 1 ,以处理 rejected 状态。

promise 0fulfilled 时,执行匿名闭包,进而执行用户闭包,从而创建一个临时的 promise m。此时,promise m 通过 pipe(to:) 方法将 promise 1 的状态更新方法(任务)box.seal 加入回调任务列表,最终形成上图所示的内存引用关系。

Guarantee

Guarantee 其实是裁剪版的 Promise,不支持错误处理,即 保证有返回值 的含义。

Guarantee 遵循 Thenable 协议,并重新实现了一套链式操作符(方法),不同的是,返回值为 Guarantee 类型,从而避免交错使用 PromiseGuarantee

全局方法

为了便于使用,PromiseKit 还提供了几个常用的全局方法,包括:

  • after:延时任务,Guarantee 类型。
  • firstly:语法糖,让代码更具可读性,立即执行闭包。
  • race:当有一个 Promise 为 fulfilled 状态时,执行回调任务列表。
  • when:当所有 Promise 均为 fulfilled 状态时,执行回调任务列表。

总结

整体而言,PromiseKit 以 ThenableCatchMixin 协议为基础,实现了两种类型的 Promise,分别是 PromiseGuarantee

Promise 的工作流程主要涉及了 BoxResolver,两者有各自的职责。Box 包含了执行结果、执行状态、回调任务列表,并提供了状态更新方法 sealResolver 作为执行器,提供给用户来更新状态。

Thenable 提供的 pipe(to:) 方法是串联任务的关键,几乎所有的链式操作符均使用了 pipe(to:) 方法进行任务串联。

从编码角度而言,PromiseKit 在职责单一、方法命名方面的设计与实践,还是非常值得我们来学习的。

后续,我们将继续阅读一些不错的开源源码,学习其中的设计思想。

参考

  1. PromiseKit
  2. Promise 核心实现原理
欣赏此文?求鼓励,求支持!