本文是 『 Swift 新并发框架 』系列文章的第二篇,主要介绍 Swift 5.5 引入的 actor。
©原创文章,转载请注明出处!
本系列文章对 Swift 新并发框架中涉及的内容逐个进行介绍,内容如下:
Swift 新并发框架之 actor
Overview
Swift 新并发模型不仅要解决我们在『 Swift 新并发框架之 async/await 』一文中提到的异步编程问题,它还致力于解决并发编程中最让人头疼的 Data races 问题。
为此,Swift 引入了 Actor model :
Actor 代表一组在并发环境下可以安全访问的(可变)状态;
Actor 通过所谓数据隔离 (Actor isolation) 的方式确保数据安全,其实现原理是 Actor 内部维护了一个串行队列 (mailbox),所有涉及数据安全的外部调用都要入队,即它们都是串行执行的。
为此,Swift 引入了 actor
关键字,用于声明 Actor 类型,如:
1 | actor BankAccount { |
除了不支持继承,actor
与 class
非常类似:
引用类型;
可以遵守指定的协议;
支持 extension 等。
当然了,它们最大的区别在于 actor 内部实现了数据访问的同步机制,如上图所示。
Actor isolation
所谓 Actor isolation 就是以 actor 实例为单元 (边界),将其内部与外界隔离开。
严格限制跨界访问。
跨越 Actor isolation 的访问称之为 cross-actor reference,如下图所示:
cross-actor reference 有 2 种情况:
引用 actor 中的 『 不可变状态 (immutable state) 』,如上面例子中的
accountNumber
,由于其初始化后就不会被修改,也就不存在 Data races,故即使是跨界访问也不会有问题;引用 actor 中的 『 可变状态 (mutable state)、调用其方法、访问计算属性 』 等都被认为有潜在的 Data races,故不能像普通访问那样。
如前所述,Actor 内部有一个
mailbox
,专门用于接收此类访问,并依次串行执行它们,从而确保在并发下的数据安全。从这里我们也可以看出,此类访问具有『 异步 』特征,即不会立即返回结果,需要排队依次执行。
因此,需要通过
await
执行此类访问,如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class AccountManager {
let bankAccount = BankAccount.init(accountNumber: 123456789, initialDeposit: 1_000)
func depoist() async {
// 下面的 bankAccount.accountNumber、bankAccount.deposit(amount: 1) 都属于cross-actor reference
// 对 let accountNumber 可以像普通属性那样访问
//
print(bankAccount.accountNumber)
// 而对于方法,无论是否是异步方法都需通过 await 调用
//
await bankAccount.deposit(amount: 1)
}
}
当然,更不可能 cross-actor 直接修改 actor state:
1 | func depoist() async { |
nonisolated
Actor 内部通过 mailbox 机制实现同步访问,必然会有一定的性能损耗。
然而,actor 内部的方法、计算属性并不一定都会引起 Data races。
为了解决这一矛盾,Swift 引入了关键字 nonisolated
用于修饰那些不会引起 Data races 的方法、属性,如:
1 | extension BankAccount { |
当然了,在nonisolated
方法中是不能访问 isolated state 的,如:
1 | extension BankAccount { |
在 actor 内部,无论是否是 nonisolated,各方法、属性都可以直接访问,如:
1 | extension BankAccount { |
但需要注意的是,正如前面所述,Actor isolation 是以 actor 实例为边界,如下是有问题的:
1 | extension BankAccount { |
other
相对于self
来说属于另一个 actor 实例,故不能直接跨界访问。
Actor reentrancy
为了避免死锁、提升性能,Actor-isolated 方法是可重入的:
Actor-isolated 方法在显式声明为异步方法时,其内部可能存在暂停点;
当 Actor-isolated 方法因暂停点而被挂起时,该方法是可以重入的,也就是在前一个挂起被恢复前可以再次进入该方法;
1 | extension BankAccount { |
1 | class AccountManager { |
1 | Withdrawal succeeded, balance = 400.0 |
上述结果显然是不对的。
一般的,check—reference/change 二步操作不应跨 await suspension point。
因此,fix 也很简单,在真正 reference/change 前再 check 一次:
1 | func withdraw(amount: Double) async throws -> Double { |
1 | Withdrawal succeeded, balance = 400.0 |
总之,在开发过程中要注意 Actor reentrancy 的问题。
globalActor/MainActor
如前文所述,actor 是以其实例为界进行数据保护的。
但,如下,若需要对全局变量 globalVar
、静态属性 currentTimeStampe
、以及跨类型 (ClassA1
、ClassA2
)/跨实例进行数据保护该如何做?
1 | var globalVar: Int = 1 |
这正是 globalActor
要解决的问题。
currentTimeStampe
虽定义在 actorBankAccount
中,但由于是static
属性,故不在 actor 的保护范围内。
也就是不属于BankAccount
的 actor-isolated 范围。因此,可以在任意地方通过
BankAccount.currentTimeStampe
访问、修改其值。
1 | @globalActor |
如上,定义了一个 global actor:MyGlobalActor
,几个关键点:
global actor 的定义需要使用
@globalActor
修饰;@globalActor
需要实现GlobalActor
协议:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2110.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) (macOS
public protocol GlobalActor {
/// The type of the shared actor instance that will be used to provide
/// mutually-exclusive access to declarations annotated with the given global
/// actor type.
associatedtype ActorType : Actor
/// The shared actor instance that will be used to provide mutually-exclusive
/// access to declarations annotated with the given global actor type.
///
/// The value of this property must always evaluate to the same actor
/// instance.
static var shared: Self.ActorType { get }
/// The shared executor instance that will be used to provide
/// mutually-exclusive access for the global actor.
///
/// The value of this property must be equivalent to `shared.unownedExecutor`.
static var sharedUnownedExecutor: UnownedSerialExecutor { get }
}在
GlobalActor
协议中,一般我们只需实现shared
属性即可 (sharedUnownedExecutor
在GlobalActor
extension 中有默认实现);global actor (本例中的
MyGlobalActor
) 本质上是一个 marker type,其同步功能是借助shared
属性提供的 actor 实例完成的;global actor 可用于修饰类型定义 (如:class、struct、enum,但不能用于 actor)、方法、属性、Closure等。
1
2
3
4// 在闭包中的用法如下:
Task { @MyGlobalActor in
print("")
}
1 | @MyGlobalActor var globalVar: Int = 1 |
如上,可以通过 @MyGlobalActor
对它们进行数据保护,并在它们间形成一个以MyGlobalActor
为界的 actor-isolated:
在
MyGlobalActor
内部可以对它们进行正常访问,如ClassA2.testA2
方法所做;在
MyGlobalActor
以外,需通过同步方式访问,如:await globalVar
。
UI 操作都需要在主线程上执行,因此有了 MainAcotr,几个关键点:
MainActor 属于 globalAcotr 的特例;
1
@globalActor final public actor MainActor : GlobalActor
被 MainActor 修饰的方法、属性等都将在主线程上执行。
还记得在『 Swift 新并发框架之 async/await 』一文中提到的异步方法在暂停点前后可能会切换到不同线程上运行吗?
被 MainActor 修饰的方法是个例外,它一定是在主线程上执行。
除了用 @MainActor
属性外,我们也可以通过 MainActor.run
在主线程上执行一段代码:
1 | extension MainActor { |
如:
1 | await MainActor.run { |
谨防内部幺蛾子
至此,我们知道 actor 是通过 mailbox 机制串行执行外部调用来保障数据安全。
言外之意就是如果在 actor 方法内部存在 Data races,它是无能为力的,如:
1 | actor BankAccount { |
如上面这段代码(故意捏造的),由于BankAccount.deposit
内部手动开启了子线程 (第 9 ~ 13 行),故存在 Data races 问题,会 crash。
一般地,actor 主要用作 Data Model,不应在其中处理大量业务逻辑。
尽量避免在其中手动开启子线程、使用GCD等,否则需要使用传统手法 (如 lock) 解决因此引起的多线程问题。
规避外部陷阱
说完内忧,再看外患!
正如前文所讲,Actor 通过 mailbox 机制解决了外部调用引起的多线程问题。
但是…,对于外部调用就可以高枕无忧了吗?
1 | class User { |
1 | class AccountManager { |
注意上面这段代码在编译时编译器给的 Warning:
Non-sendable type ‘User’ returned by implicitly asynchronous call to actor-isolated instance method ‘user()’ cannot cross actor boundary.
所有与 Sendable 相关的 warning 都需要 Xcode 13.3 才会报。
先抛开什么是 Sendable 不谈
这个 warning 还是很好理解的:
User 是引用类型(class);
通过 actor-isolated 方法将 User 实例传递到了 actor 外面;
此后,被传递出来的 user 实例自然得不到 actor 的保护,在并发环境下显然就不安全了。
通过参数跨 actor 边界传递类实例也是同样的问题:
1 | extension actor BankAccount { |
当然了,跨 actor 传递函数、闭包也是不行的:
1 | extension BankAccount { |
除了这些 warning,还有货真价实的 crash:
1 | extension User { |
如上,虽然 BankAccount
是 actor
类型,且其内部没有开启子线程等『 非法操作 』,
但在调用 User.testUser(callback: @escaping () -> Void)
后会 crash。
怎么办?
这时就要轮到 Sendable 登场了:『 Swift 新并发框架之 Sendable 』
小结
actor 是一种新的引用类型,旨在解决 Data Races;
actor 内部通过 mailbox 机制实现所有外部调用的串行执行;
对于明确不存在 Data Races 的方法、属性可以使用
nonisolated
修饰使之成为『 常规 』方法,以提升性能;通过
@globalActor
可以定义全局 actor,用于对全局变量、静态变量、多实例等进行保护;actor 内部尽量避免开启子线程以免引起多线程问题;
actor 应作 Data Model 用,不宜在其中处理过多业务逻辑。
参考资料
swift-evolution/0296-async-await.md at main · apple/swift-evolution · GitHub
swift-evolution/0304-structured-concurrency.md at main · apple/swift-evolution · GitHub
swift-evolution/0306-actors.md at main · apple/swift-evolution · GitHub
Understanding async/await in Swift • Andy Ibanez
Concurrency — The Swift Programming Language (Swift 5.6)
Connecting async/await to other Swift code | Swift by Sundell