《Swifter Swift 异步编程》读书总结
针对 Onevcat 所著《SWIFTER Swift 异步和并发》做阅读摘要总结
1. Swift 并发初步
主要描述一些基本的串行,并行的历史背景. 提到了传统异步操作的一些问题:
- completion 接口无法保证只调用一次
- 错误隐藏,无法直接 throw
- 缺乏系统化的取消机制 (“hmm 感觉这里有点牵强”)
并在串行并行问题的历史回顾的基础上,进一步引入 Swift Concurrency: “Swift 提供内建的支持,让开发者能以结构化的方式书写异步和并行的代码,并发这个术语,指的是异步和并行这一常见组合”
其实无论是并行/串行,还是后续的 Semaphore/Actor/Channel 等模式,解决的问题集中归纳为:
- 如何保证不同运行步凑之间按正确顺序进行 (逻辑正确)
- 如何保证资源能够在不同运算之间安全的共享 (内存安全)
简单的描述了 async/await 的优势. 主要是降低 closure 的层数. 在此也简单的提到了 语法糖 async let 的详细使用, 具体细节可参考 5.1.2 Asybc Let 的内容.
2. 创建异步函数
还是基于第一部分章节提到的现有的串行并行设计的局限性,讲了一下 async await 模式的优点
- 避免回调地狱
- 不需要关心线程调度,async/await 对线程调度进行了封装
- 避免因为第二点所造成的过度创建线程以及过度跨线程调用所导致的性能损耗
2.1 阐述 await 关键字的作用
“await 充当的角色,就是标记出一个潜在的暂停点 (suspend point)。”
在异步函数中,可能发生暂停的地方,编译会要求我们明确使用 await 将它标记出来。除此之外,await 并没有其他更多的语义或是运行时的特性。当控制权回到异步函数中时,它会从之前停止的地方开始继续运行。但是“桃花依旧笑春风”的同时,“人面不知何处去”也会是一个事实:虽然部分状态,比如原来的输入参数等,在 await 前后会被保留,但是返回到当前异步函数时,它并不一定还运行在和之前同样的线程中,异步函数所在类型中的实例成员也可能发生了变化。await 是一个明确的标识,编译器强制我们写明 await 的意义,就是要警示开发者,await 两侧的代码会处在完全不同的世界中
此处着重讨论了对于同步函数转为异步时需要注意的一些场景:
- 对于闭包的同步函数如果带有返回值,考虑通过 inout 的方式把原返回值放入函数参数中
- 建议从下而上的提供异步函数重构,对于一个已经提供异步函数的同步函数版本,可以添加注释:
@completionHandlerAsync
来提醒 api 使用方,当前函数已经有异步函数版本可以使用 - 如果需要封装现有的同步函数为异步函数, 可以使用 checkedContinuation 或者 unsafeContinuation
- 对于 continuation 来说,因为本身其实也是 sendable 协议的遵循者,因此 continuation 可以被创建 scope 以外的地方暂存
protocol WorkDelegate {
func workDidDone(values: [String])
func workDidFailed(error: Error)
}
class Worker: WorkDelegate {
var continuation: CheckedContinuation<[String], Error>?
func doWork() async throws -> [String] {
try await withCheckedThrowingContinuation({ continuation in
self.continuation = continuation
performWork(delegate: self)
})
}
// 注意这里continuation只可以被调用一次, 因此调用之后需要设置为nil
func workDidDone(values: [String]) {
continuation?.resume(returning: values)
continuation = nil
}
func workDidFailed(error: Error) {
continuation?.resume(throwing: error)
continuation = nil
}
}
关于 async 异步函数与 Future 的对比, future 其实也是一个用于返回未来的特定值的一个 observable,并且支持 concurrency 异步函数调用
let number = await generateAsyncRandomNumberFromFuture().value
2.2 ObjC / Swift 交互
这部分总结了 Swift <-> ObjC 之间的相互转换问题:
先看看 ObjC 到 Swift:
- 如果一个 Objective-C 函数存在函数参数,且该参数的返回值和整个函数本身的返回值类型都为 void 的话,该 Objective-C 函数在 swift 中会自动提供对应的异步执行函数。
- 如果 ObjC 函数不希望自动生成 async 异步函数,可以在函数结尾声明:
NS_SWIFT_DISABLE_ASYNC
再看看 Swift 到 ObjC:
- 如果一个 Swift 函数被标注为@objc,如果该函数为 async 函数,那么会自动生成对应的带有 completion hander 的 ObjC 同步函数。
2.3 一些 Async 的语法糖
“谈谈 Async Getter 以及 Async 下标支持”
Swift 5.5 以后支持对下标/getter 的异步操作。一个简单的例子,actor 中的 isolate 属性都需要通过 await 获得:
- 异步下标支持:
// File 通过下标支持异步查询属性的列子
class File {
subscript(_ attribute: AttributeKey) -> Attribute {
// 比如 await file[.readonly] == true
get async {
let attributes = await loadAttributes()
return attributes[attribute]
}
}
}
- 异步 getter 支持:
// File 通过异步getter支持计算文件大小的例子
class File {
var size: Int {
get async throws {
if corrupted {
throw FileError.corrupted
}
try await heavyOperation()
}
}
func heavyOperation() async throws -> Int {
// ...
}
}
2.4 状态依赖
状态量在 await 前后很可能发生变更,比如说下面的代码,在 prepare 之前跟之后的 loaded 值可能不一样,在后续的实现中,这种一致性需要通过 actor 来统一:
var loaded: Bool = false
var shouldLoad: Bool {
get async {
if !loaded {
await prepare()
return true
}
return false
}
}
func load() {
loaded = true
}
3. 异步序列
类似于同步序列,核心为异步迭代器, 为异步序列定义类似的结构,也是为了让开发在 for…in 中进行异步调用.
3.1 异步迭代器
并且对于同一个 async sequence, 进行多次 for loop, 每一次的 sequence 应该是一致的,因为每次 for 遍历,都是在重新创建 iterator. 对于自定义的 async sequence 来说,如果需要保持单次遍历, 比如说 I/O, 事件流等等, 则需要持有创建后的 iterator,避免 iterator 的重复调用.
这里可以仔细观察一几个协议:
// AsyncSequence & AsyncIterator
// 两个协议被拆开,其实实现上,可以考虑放在一起.
struct Counter: AsyncSequence {
typealias Element = Int
let howHigh: Int
struct AsyncIterator: AsyncIteratorProtocol {
let howHigh: Int
var current = 1
mutating func next() async -> Int? {
// A genuinely asynchronous implementation uses the `Task`
// API to check for cancellation here and return early.
guard current <= howHigh else {
return nil
}
let result = current
current += 1
return result
}
}
func makeAsyncIterator() -> AsyncIterator {
return AsyncIterator(howHigh: howHigh)
}
}
3.2 异步序列操作
异步操作序列与同步操作序列支持的操作符类似:
let seq = AsyncFononacciSequnece()
.filter { $0.isMultipleOf(2) }
.prefix(5)
for try await v in seq {
print(v)
}
但对于协议设计上来说, Sequence 协议跟 AsyncSequence 本质上有很大区别.
- Sequence 是在 map 这类操作过程中,对 Sequence 对应的数组进行递归
- AsyncSequence 在 map 这类操作过程中, 创建一个封装之后的新的 AsyncSequence 对象. 在迭代器通过 await next 时,通过层层递归的 transform 来获得最终的值. 更加类似于 LazySequence 的设计模式.
extension AsyncSequence {
func map<Transformed>(
_ transform: @escaping (Self.Element) async -> Transformed
) -> AsyncSequence<Self, Transformed>
}
往往以上的 transform 会缠上多层 generic asyncSequenceType, 此时,就可以通过 any AsyncSequence 进行模糊处理.
3.3 Async Stream
为了服务将一个任务下的多次异步操作转换为一个异步序列的操作的场景,Swift 官方提供了 Async Stream. 比如下面一个列子:
var timerStream: AsyncStream<Date> {
AsyncStream<Date> { continuation in
let initial = Date()
// 1
Task {
let t = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: true
) { timer in
let now = Date()
print("Call yield")
continuation.yield(Date())
let diff = now.timeIntervalSince(initial)
if diff > 10 {
print("Call finish")
continuation.finish()
}
}
// 2
continuation.onTermination = { @Sendable state in
print("onTermination: \(state)")
t.invalidate()
}
}
}
}
“但凡基于 push 的消息/同步模式,都会存在推送与消费速度不一致的问题”
因此 Async Stream 也存在对应的 backpressure 问题。类似于其他的诸多类似的模式中的方案(比如说 RxJava),AsyncStream 本身也提供缓冲(backpressure) 策略, 当多个数据被 yield, 但是 for await 还没有能够消化能够及时处理这些值的话,他们会被放到 buffer 中,这里会存在来不及及时处理导致 buffer 过大。 因此可以使用 AsyncStream 的构建选项: 通过 bufferPolicy 来调整 buffer 策略。
AsyncStream 在内部实现了一个由互斥锁保护的高效队列,用来作为 yield 调用的缓冲区。在每次 for await 时,AsyncStream 的迭代器 (不要忘记 AsyncStream 是满足异步序列协议的) 的 next 方法会向这个内部存储队列请求一个值,并将它返回。只有在缓冲区中没有值时,这个 await 才进入真正的等待状态
对应 buffer 策略默认为 unbounded, 但是可以修改为最新 xx 条 / 最旧 xx 条. 这里也可以跟 Combine 对比一下,因为 combine 本身支持推拉结合的方式,在 pull 模式下可以告知 Publisher 当前的 Subscriber 是否需要接受下一条消息.
enum BufferingPolicy {
case unbounded
case bufferingOldest(Int)
case bufferingNewest(Int)
}
注意: AsyncStream 不可以同时在多个 Task 上被递归!
3.4 异步序列与 Combine
因为 AsyncStream 的本质与 Combine 中的 Publisher 模型极为相似, 因此实际上可以相互转换. 也可以作为数据层(concurrency)
extension Publisher {
var asAsyncStream: AsyncThrowingStream<Output, Error> {
AsyncThrowingStream(Output.self) { continuation in
let cancellable = sink { completion in
switch completion {
case .finished:
continuation.finish()
case .failure(let error):
continuation.finish(throwing: error)
}
} receiveValue: { output in
continuation.yield(output)
}
continuation.onTermination = {
@Sendable _ in
cancellable.cancel()
}
}
}
}
总结一下 Combine 与 AsyncStream 的差异:
- 异常处理: Combine 是通过回掉的方式(failure), Concurrency 中的 AsyncStream 是通过 Throw.
- 调度执行: Combine 的调用是通过 scheduler 协议执行, Concurrency 是通过 Task + Actor 保证, 后者对 concurrency 的控制更加抽象.
4. 异步函数常见实用场景
“作为一个新的框架,总要看看如何与 iOS 的其他框架进行对接的”
4.1 URLSession
URLSession, 提供 URLSession.shared.data(from: url) 异步函数,同时也支持 Byte 层级的方法:
let url = URL(string: "https://example.com")!
let session = URLSession.shared
let (bytes, response) = try await session.bytes(from: url)
for try await byte in bytes {
print(byte, terminator: ",")
}
4.2 Notification
Notification 不再需要进行注册,而是直接通过提供一个 AsyncSequence, 异步提供 incoming 的通知事件.
Task {
let backgroundNotifications = NotificationCenter.default.notifications(
named: UIApplication.didEnterBackgroundNotification,
object: nil
)
if let notification = await backgroundNotifications.first(where: {_ in true}) {
print(notification)
}
}
// task 需要在恰当大事记进行cancel,来让序列终结,避免泄露
task.cancel()
4.3 关于@main 入口
App 或者 Mac 程序可以标记入口为 @main, 代此标记的函数会被真正的 main 所调用. 相当于在真正的 main 中运行了.
// 一段使用@main 标注的main函数
@main
struct MyApp {
static func main() async {
await Task.sleep(NSEC_PER_SEC)
print("Done")
}
}
// 实际上通过编译会被封装成以下的一段代码
func main() {
_runAsyncMain { await MyApp.asyncMain() }
}
4.4 SwiftUI 的支持
为了能在 SwiftUI 中使用异步函数, View 中提供了 task 的异步函数入口, task 函数的调用时机与 onAppear 相同。该设计将允许 View 出现在 View 层级上时,执行相关的异步操作。
// 这里通过View 提供task 函数的好处在于
// 当view 从屏幕上被移除时,task 所关联的任务也将会被取消
@State private var result = ""
var body: some View {
ProgressView()
.task {
let value = try? await load()
result = value ?? "<nil>"
}
Text(result)
}
func load() async throws -> String {
// 模拟加载时间,比如从网络获取数据
try await Task.sleep(nanoseconds: NSEC_PER_SEC)
return "Hello World"
}
5. 结构化并发 & 任务取消
“对结构化 / 非结构化的概念进行探讨,并浅谈任务取消的一些小技巧”
结构化的编程: 使用标准的条件语句以及代码块的方式,进行逻辑执行. 结构化的并发: 避免使用 complete call 的方式(这里类比于非结构化编程中使用 goto 的方式进行回调并发)
非结构化的并发存在的问题,是希望被结构化的并发锁解决的:
- 函数 callback 之后是否还会继续运行?
- 作为调用者,应该在什么线程处理这个回调
- 回调提供的资源如何管理?
- 如何取消这个执行的异步任务以及它的子任务.
5.1 结构化 Task 并发模型
获取当前任务状态: withUnsafeCurrentTask, 既可以在 Task 中,也可以在 Task 所调用的函数中获取当前的 task 对象,并进一步对 task 进行相关的操作:
func foo() async {
withUnsafeCurrentTask { task in
// ...
}
syncFunc()
}
func syncFunc() {
withUnsafeCurrentTask { task in
print(task as Any)
// => Optional(
// UnsafeCurrentTask(_task: (Opaque Value))
// )
}
}
在任务并发中,有两种方式创建子从属任务:
- async let
- task group.
主从任务模式的优势在于:
- 父任务只有在子任务完结后才会完结,
- 父任务完成取消操作时, 子任务的状态也会自动设置为取消.
5.1.1 任务组:
“最为常见的一种层级式任务启动方式”
下面的代码可以代表一种常见的 Task Group 的使用场景:
/**
以下代码执行顺序为:
// 输出:
// Start
// Task added
// Start work 0
// Start work 1
// Start work 2
// Work 0 done
// Get result: 0
// Work 1 done
// Get result: 1
// Work 2 done
// Get result: 2
// Task ended
// End
*/
struct TaskGroupSample {
func start() async {
print("Start")
await withTaskGroup(of: Int.self) { group in
for i in 0 ..< 3 {
group.addTask {
await work(i)
}
}
print("Task added")
for await result in group {
print("Get result: \(result)")
}
print("Task ended")
}
print("End")
}
private func work(_ value: Int) async -> Int {
print("Start work \(value)")
await Task.sleep(UInt64(value) * NSEC_PER_SEC)
print("Work \(value) done")
return value
}
}
这里值得注意的是,就算没有 for await in
的调用,上述 Task Group 也会在 group 中的所有 task 结束后,再返回. 其原因在于: 编译器在检测到结构化并发作用域结束时,会为我们自动添加上 await 并在等待所有任务结束后再继续控制流
注意: 通过 Task.withUnsafeTaskGroup 中创建的 group 不应该被外部持有, 如果 group 被外部所使用的话, 会导致 group 对象可能在 group 任务完成后继续进行 addTask. 这种行为是未定义的, 在未来的 Swift Concurrency 库版本中将不保证该操作的安全性 因此不建议这么做
5.1.2 Async Let:
“比 group 更加方便的 async 语法糖”
Task Group 提供了一种正规化的方式,在同一个块中集中对一个 group 下的任务进行创建.
func start() async {
print("Start")
async let v0 = work(0)
async let v1 = work(1)
async let v2 = work(2)
print("Task added")
let result = await v0 + v1 + v2
print("Task ended")
print("End Result: \(result)")
}
async let
与 task group
调用方式的区别: 大多数情况下可以互换,不过 async let 不便于动态的表达任务数量,一般来说生成籽任务的数量是在编译时已经确定好的,这一点上 TaskGroup 更有优势
5.1.3 结构化并发的组合
在实际使用过程中,往往可以使用 async let 以及 group 组合的方式创建树状任务结构, 便于更加精确的控制任务状态.
对于上面使用 work 函数的例子来说,多加的一层 innerGroup 在执行时并不会造成太大区别:三个任务依然是按照结构化并发执行。不过,这种层级的划分,给了我们更精确控制并发行为的机会。在结构化并发的任务模型中,子任务会从其父任务中继承任务优先级以及任务的本地值 (task local value);在处理任务取消时,除了父任务会将取消传递给子任务外,在子任务中的抛出也会将取消向上传递
5.1.4 非结构化任务
“Swift Concurreny 的本质是结构化并发管理,因此绝大多数场景都应该避免这样的使用”
除非有特别的理由希望某个任务独立于结构化并发的生命周期,否则我们应该尽量避免在结构化并发的上下文中使用非结构化任务。这可以让结构化的任务树保持简单,而不是随意地产生不受管理的新树 :p (就是不要在 Task 中循环创建顶层 Task)
5.2 协作式任务取消
“任务取消的原理,技巧,以及注意事项”
在 Swift 中,对于任务触发 Cancel,只会对该任务进行两个操作:
- 自身的 isCancel 设置为 true
- 在结构化并发中, 如果任务有子任务,对子任务调用 cancel.
至于子任务是否 cancel,取决于子任务本身对 isCancel 的检查. 取消的方式可以通过
- 检查 isCancel 的方式,返回正常兜底值
- 执行 checkCanceled 的方式,抛出错误, 由顶层 Task 的创建者的所在块进行捕获.
下图中, 当对 SubTask 执行取消操作后,其子任务取决于对 isCancel 或者 checkCanceled 的判断,根据自身的实现决定是否需要终止操作。
5.2.1 任务取消实战
接下来让我们来考虑一个复杂一点的结构化并发例子。现在的 work 函数需要处理的字符串是硬编码写死的 “Hello”。改写一下这个函数,让它接受任意的字符串输入, 并利用新的这个 work 函数构建一颗复杂的结构化的并发任务树:
do {
let value: String =
try await withThrowingTaskGroup(of: String.self) {
group in
// Task 1
group.addTask {
try await withThrowingTaskGroup(of: String.self) {
inner in
// Task 1.1
inner.addTask { try await work("Hello") }
// Task 1.2
inner.addTask { try await work("World!") }
// 取消任务组 inner
await Task.sleep(UInt64(2.5 * Double(NSEC_PER_SEC)))
inner.cancelAll()
return try await inner.reduce([]) {
$0 + [$1]
}.joined(separator: " ")
}
}
// Task 2
group.addTask {
try await work("Swift Concurrency")
}
return try await group.reduce([]) {
$0 + [$1]
}.joined(separator: " ")
}
print(value)
} catch {
print(error)
}
func work(_ text: String) async throws -> String {
var s = ""
for c in text {
if Task.isCancelled {
print("Cancelled: \(text)")
}
try Task.checkCancellation()
await Task.sleep(NSEC_PER_SEC)
print("Append: \(c)")
s.append(c)
}
print("Done: \(s)")
return s
}
在 inner 被执行 cancelAll 中,由于 work 会抛出异常 ,因此被最上层的 group 对象所捕获。在结构化的并发设计中,当异常被捕获后,会先将其他运行的子任务进行 cancel。 然后等待其他剩余子任务完成后,再将首先接到的这个 error 抛到外层。 整个流程入下图所示:
5.2.2 关于任务取消后的清理
使用 defer:
与同步函数一样,也可以使用 defer,保证在代码块的最后进行相关的内存回收工作. 在使用 defer 时,只有在异步操作返回或者抛出时,defer 才会被触发
func load(url: URL) async throws {
let started = url.startAccessingSecurityScopedResource()
if started {
defer {
url.stopAccessingSecurityScopedResource()
}
await doSomething(url)
try Task.checkCancellation()
await doAnotherThing(url)
try Task.checkCancellation()
}
}
使用 cancellationHandler:
因为 defer 有一定的滞后效果, 只有在 cancel 之后或者返回的时候触发,时机会比任务 cancel 的时机晚. 因此也可以考虑可选的采用: withTaskCancelationHandler
函数,在 task cancel 的当前时刻进行拦截.
func withTaskCancellationHandler<T>(
operation: () async throws -> T,
onCancel handler: @Sendable () -> Void
) async rethrows -> T
使用 addTaskUnlessCanceled:
当创建子任务会消耗额外资源并且一旦创建中途无法取消的时候,建议使用该方法。这是因为传统的 addTask 方法在任务添加后,依然需要依赖 Task 自身检查 isCanceled 属性来决定是否执行当前任务。 如果使用 addTaskUnlessCanceled, 当 group 已经被 cancel 后,则不会添加对应的任务到 task group 中。
6. 浅谈 Actor 模型与数据隔离
“保证数据操作在 Concurrency 框架中的安全”
传统的锁隔离,通过将锁分配给对应的执行线程,来保证操作安全性,锁面临的问题在于:
- 如果没有足够的锁,对象状态那么可能由于多线程访问而损坏
- 如果设计太多锁,锁本身也会带来性能损耗
Actor 隔离,将数据隔离在特定操作员(actor)能够操作的范畴中,由不同线程与 actor 通讯,让 actor 对数据进行操作。由 Swift 本身保证该模式下通讯的高效性。 所有 Actor 都隐式的遵循 Actor 协议,其定义为:
protocol Actor : AnyObject, Sendable {
nonisolated var unownedExecutor: UnownedSerialExecutor { get }
}
Actor 属于典型的 Sendable 协议实现者,而对于 Sendable 来说,满足其协议需要符合以下条件:
- actors 默认支持.
- value type: 比如说 enum/struct 默认支持.
- 如果支持 generic type, 那么 generic type 本身需要支持 sendable 协议.
- class: immutable classes 或者 internal locking guarded. 对于特殊 case 需要使用@unchecked 关键字表明并发安全由内部逻辑保证
- 对于 Sendable 类型的 closure 来说,任何 capture 类型都需要为 sendable.
本章的剩下篇幅用于解释 isolated 与 nonisolated 概念,以及 协议与 actor 类是配的问题。前者关于 isolate 以及非 isolate 的概念对比,可以参考这片文章 不在此展开。这里集中总结一下当 actor 与传统协议对接时面临的问题:
protocol Popular {
var popular: Bool { get }
}
// 编译错误: actor-isolated property 'popular' cannot be used to satisfy a protocol requirement
actor Room {
var popular: Bool {
visitorCount > 10
}
}
两种处理以上问题的方式:
- 相关 protocol 集成 actor 协议
- protocol 中的原有协议改为 async 方式执行
7. 并发线程模型
“Under The Hood”
7.1 总结
Swift Concurrency 目前提供的了种工具,来解决 **5.结构化并发 **中阐述的,需要解决的问题。
- 异步函数调用
- 结构化并发,保证调用时序
- actor & sendable 保证数据一致性以及多线程安全.
“Concurrency 的本质”
Swift Concurrency 并发在底层使用的是一种新实现的协同式线程池 (cooperative thread pool) 的调度方式: 由一个串行队列负责调度工作,它将函数中剩余的运行内容被抽象为更轻量的续体 (continuation),来进行调度
“传统的 GCD 模式的局限性”
在 Concurrency 库出现直线,Swift 是采用的 GCD 方式,将不同 thread 运行在不同 core 上, 可能出现多个 thread 在一个内核上运行的 case. 单个线程的内存占用达到 500KB - 1MB, 因此线程创建本身也是一件带有内存消耗的事情. 在并发 queue 中如果任务未完成执行,那么 queue 会倾向于重新创建 thread,直到达到并发队列的上限(64) 为止.
“异步线程模型小结”
调度库将续体暂存在堆上,并在需要的时候用它“替换”掉调度队列线程的运行栈,是异步函数拥有放弃线程能力的基础:
- 在调度线程空闲时 (比如 await 后),执行器会为它寻找接下来需要处理的指令,
- 这个指定可能是 await 所需要执行的部分,也可能是和之前完全不相关的其他任务。
- 和传统 GCD 调度的资源抢占式不同,这种调度方式通过协作的方式,由执行器、需要处理的工作和调度队列一同来保证线程向前运行,这也是我们把它叫做协同式线程池的原因。
7.2 锁/信号量
对于现有的锁/信号量的异步调用的建议
- 对于锁: 如果要在 Task 中使用锁,那么锁的 lock 以及 unlock 需要在两段 await 的中间. (which makes sense)
- 对于信号量: 由于 DispatchSemaphore 讲无条件组赛当前线程,由于 Task 本身对执行线程进行了抽象, 可能导致相关的其他 task 中对 Semaphore 进行更新的行为也被阻塞,因此不建议使用信号量
7.3 执行器
Swift Concurrency 提供两种对应的执行器, 一种是全局并发执行器,一种是对于每个 actor 提供的串行执器, 保证 actor 在自己的串行任务队列中执行相关的数据操作函数.
- 全局并发执行器: 操作系统提供的闭源并发线程池
- Actor 执行器: 每一次 actor 对于函数的带哦用,都会执行对应 SerialExecutor 的 enqueue job 操作.