概览
在 Swift 结构化并发构成的体系中,一个称为“演员”(Actor)的成员扮演了非常重要的角色,它被用来隔离和同步执行中的数据。
除了普通 Actor 以外,还有一个全局“演员”(Global Actor)的概念,它是做什么的?又有什么与众不同的长处呢?
在本篇博文中,您将学到如下内容:
- MainActor:“我是主角!”
- 何为全局 Actor?
- “开始你的表演!”
掌握 Swift 结构化并发是迈向现代化并行开发模型的必由之路,而 Actor 以及 Global Actor 又是其中不可或缺的重要元素!
那还等什么呢?小伙伴们,Let‘s playing!!!;)
1. MainActor:“我是主角!”
我相信即使是 Apple 开发中头发浓密的初学者们也肯定都知道:所有和界面相关的操作一定要在主线程(Main Thread)中完成。
对于 Swift 结构化并发中的 Actor 来说,这一点也不能例外。
所以,一个所谓的“绝对主角”(Main Actor)踏着七彩祥云“从天而降”来帮助我们排忧解难。在 UIKit 中,所有和界面相关的类,以及它们对应的方法和属性,默认都包容在 MainActor 之中:
在 SwiftUI 中,视图的 body 属性也很“识趣”的与 MainActor 成了好朋友:
在 Swift 结构化并发模型中, 使用 MainActor 可以确保我们的任务都毫无悬念的将会在主线程中运行。
在某些非 MainActor 的运行上下文中,我们也可以非常方便的将其切换到主线程上去:
Task {
// 非 MainActor 运行上下文
await MainActor.run {
// MainActor 运行上下文
}
}
或者,我们还可以用闭包的修饰语法糖来让编译器明白我们要在 MainActor 中运行的“坚定决心”:
Task {
await withTaskGroup(of: Void.self) {group in
group.addTask {@MainActor in
try? await Task.sleep(for: .seconds(1.0))
print(Thread.current)
// 在主线程中了,加油妆点界面吧!
}
await group.waitForAll()
}
}
如上所述,MainActor 就是一个在主线程上下文中运行的 Actor,更确切的说:它是运行在主线程中的一个全局(Global)Actor!
2. 何为全局 Actor?
所谓全局 Actor 是一种全局唯一的 Actor 类型,它用于隔离程序任何位置中的各种声明代码。
说到全局 Actor,我们很自然的就想到普通的 Actor。做个不太恰当的比喻:如果说普通的 Actor 创建出一个个数不胜数的隔绝小岛,那么全局 Actor 形成的则是一片壮丽浩瀚的隔绝大陆!
上面我们讨论过,MainActor 其实就是一个全局 Actor:
@globalActor actor MainActor : GlobalActor {
static let shared: MainActor
}
同样,我们也可以非常轻松的创建自己的全局 Actor:
@globalActor actor LuckyActor: GlobalActor {
static var shared = LuckyActor()
}
正如 MainActor 所启发的那样,我们也可以如法炮制的使用上面的 LuckyActor 全局 Actor 来隔离我们的代码流:
@LuckyActor
final class Store {
var count = 0
}
如上代码所示:我们创建了一个可观察类 Store,并且将其禁锢在 LuckyActor 这片广阔的孤岛中。
有些小伙伴们可能会觉得,如果多条指令流被同一个 Actor 所修饰就意味着它们都会运行在同一个线程中,其实这样理解是不正确的。它们仍然可能运行在完全不同的线程中,只要它们可以被“互斥”执行即可。
var i = 0
Task {
async let r0 = Task {@LuckyActor in
for _ in 0..<100000 {
i += 1
}
}.value
async let r1 = Task {@LuckyActor in
for _ in 0..<100000 {
i += 1
}
}.value
_ = await (r0, r1)
print("i is \(i)")
}
对于上面代码来说,可以肯定的是最终 i 累加的值将为 20000!实际观察可以发现,其中两个 Task 绝不能同时执行,因为它们都被同一个 Actor 所修饰:
那么问题来了:既然普通 Actor 和全局 Actor 都可以隔离代码片段,那么后者存在的真谛又在哪儿呢?
3. “开始你的表演!”
感谢小伙伴们提出这个充满睿智的问题!其实答案隐隐约约在上面已经回答过了。
虽然,普通 Actor 和全局 Actor 都能起到隔离指令流的作用,但后者可以在更精细的程度上决定我们的隔离粒度。
举个栗子,我们将之前的代码略作如下变化:
var i = 0
var (a,b,c) = (0,0,0)
actor Sum {
var value = 0
func inc() {
guard value < 10000 else { return }
value += 1
}
}
let sum = Sum()
func isDone() async -> Bool {
await sum.value >= 10000
}
Task {
async let r0 = Task {
while await !isDone() {
await sum.inc()
a += 1
}
}.value
async let r1 = Task {
while await !isDone() {
await sum.inc()
b += 1
}
}.value
async let r2 = Task {
while await !isDone() {
await sum.inc()
c += 1
}
}.value
_ = await (r0, r1, r2)
await print("total is \(sum.value)")
}
可以看到:我们通过 3 个 Task 来递增 Sum.value 的值,因为它们没有指定同步的 Actor ,所以它们会并发运行。
这些任务同时递增 Sum.value 的值并不会产生同步问题:因为 Sum 本身是一个 Actor,它会妥善处理自身属性的同步。
同样的,虽然上面 a,b,c 三个变量没有做同步隔离,但是它们只会被唯一个 Task 所写入,因此也不会造成任何同步问题!
从上面演示代码运行结果可以看到:因为它们被不同 Actor 所隔离,所以这 3 个任务获取到的时间片基本都是相等的,这可以通过 a,b,c 三个值来确定。
如果我们希望第一个任务在执行时获取更多的时间片,我们可以让后两个任务运行在同一个 Actor 上下文中。使用普通的 Actor 很难实现这一目的(除非调整任务优先级),而全局 Actor 恰恰是“最佳人选”:
Task {
async let r0 = Task {
while await !isDone() {
await sum.inc()
a += 1
}
}.value
async let r1 = Task {@LuckyActor in
while await !isDone() {
await sum.inc()
b += 1
}
}.value
async let r2 = Task {@LuckyActor in
while await !isDone() {
await sum.inc()
c += 1
}
}.value
_ = await (r0, r1, r2)
await print("total is \(sum.value)")
}
运行看一下结果,第一个任务获取到了将近一半的时间片(≈5000),而后两个任务共同分享了剩下的另一半时间片(≈2500 + 2500):
不过,上面同样的演示无法用 SwiftUI(或者 UIKit)达成,其原因是所有界面上的操作都要在主线程上做同步,所以全部任务又会被“平等对待”,这意味着它们整体占用的时间片又会趋于一致:
Task.detached {
Task {
while await !isDone() {
await sum.inc()
// 在 MainActor 上的同步会“抵消” LuckyActor 带来的影响,下同。
await MainActor.run {
a += 1
}
}
}
Task {@LuckyActor in
while await !isDone() {
await sum.inc()
await MainActor.run {
b += 1
}
}
}
Task {@LuckyActor in
while await !isDone() {
await sum.inc()
await MainActor.run {
c += 1
}
}
}
}
这就有点像量子力学中的“观察者谬误”,你不看它一切如常,你只要一观察它立马就会“判若两人”,真让人唏嘘不已。
现在,大家对全局 Actor 的特点和长处是不是都已经了然于胸了呢?棒棒哒!💯
总结
在本篇博文中,我们介绍了 Swift 结构化并发模型中全局 Actor 这一有趣话题,我们随后讨论了它与一般 Actor 的不同之处,以及全局 Actor 的应用场景。
感谢观赏,再会!8-)