本文是 『 Swift 新并发结构 』系列文章的第三篇,首要介绍 Swift 5.6 引进的 Sendable。
本系列文章对 Swift 新并发结构中触及的内容逐个进行介绍,内容如下:
-
Swift 新并发结构之 async/await
-
Swift 新并发结构之 actor
-
Swift 新并发结构之 Sendable
-
Swift 新并发结构之 Task
本文一同宣布于我的个人博客
Overview
书接前文 (『 Swift 新并发结构之 actor 』),本文首要介绍 Sendable 为何物以及怎样处理前文提到的那些问题。
/// The Sendable protocol indicates that value of the given type can
/// be safely used in concurrent code.
public protocol Sendable {}
Sendable
是一个空协议:
用于向外界声明完结了该协议的类型在并发环境下可以安全运用,更准确的说是可以安闲地跨 actor 传递。
这归于一种 『 语义 』上的要求。
像 Sendable
这样的协议有一个专有名称:『 Marker Protocols 』,其具有以下特征:
-
具有特定的语义特色 (semantic property),且它们是编译期特色而非运行时特色。
如
Sendable
的语义特色便是要求并发下可以安全地跨 actor 传递; -
协议体有必要为空;
-
不能承继自 non-marker protocols (这其实是第 2 点的延伸);
-
不能作为类型名用于
is
、as?
等操作如:x is Sendable,编译报错: Marker protocol ‘Sendable’ cannot be used in a conditional cast.
-
不能用作泛型类型的束缚,从而使某类型遵循一个 non-marker protocol,如:
protocol P { func test() } class A<T> {} // Error: Conditional conformance to non-marker protocol 'P' cannot depend on conformance of 'T' to non-marker protocol 'Sendable' extension A: P where T: Sendable { func test() {} }
咱们知道,值语义 (Value semantics) 类型在传递时 (如作为函数参数、返回值等) 是会实行复制操作的,也便是它们跨 Actor 传递是安全的。故,这些类型隐式地主动遵循 Sendable
协议,如:
-
根底类型,
Int
、String
、Bool
等; -
不含有引用类型成员的
struct
; -
不含有引用类型相关值的
enum
; -
所含元素类型符合
Sendable
协议的集结,如:Array
、Dictionary
等。
当然了,全部 actor 类型也是主动遵循 Sendable
协议的。
事实上是全部 actor 都遵循了
Actor
协议,而该协议承继自Sendable
:
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public protocol Actor : AnyObject, Sendable {
nonisolated var unownedExecutor: UnownedSerialExecutor { get }
}
class 需求主动声明遵循 Sendable
协议,并有以下束缚:
-
class 有必要是
final
,否则有 Warning: Non-final class ‘X’ cannot conform to ‘Sendable’; use ‘ @unchecked Sendable’ -
class 的存储特色有必要是 immutable,否则有 Warning: Stored property ‘x’ of ‘Sendable’-conforming class ‘X’ is mutable
-
class 的存储特色有必要都遵循
Sendable
协议,否则 Warning: Stored property ‘y’ of ‘Sendable’-conforming class ‘X’ has non-sendable type ‘Y’ -
class 的先人类 (如有) 有必要遵循
Sendable
协议或者是NSObject
,否则 Error: ‘Sendable’ class ‘X’ cannot inherit from another class other than ‘NSObject’。
以上这些束缚都很好了解,都是保证完结了 Sendable
协议的类数据安全的必要保证。
回到上面那个比方:
extension AccountManager {
func user() async -> User {
// Warning: Non-sendable type 'User' returned by implicitly asynchronous call to actor-isolated instance method 'user()' cannot cross actor boundary
return await bankAccount.user()
}
}
很明显,要消除比方中的 Warning,只需让 User
完结 Sendable
协议即可。
就本例而言,User
有 2 种改造方案:
-
由 class 改成 struct:
struct User { var name: String var age: Int }
-
手动完结
Sendable
协议:final class User: Sendable { let name: String let age: Int }
回头想想,Sendable
对完结它的 class 的要求是不是太严峻了 (final、immutable property) ?!
有点过于抱负,有点不切实际
从并发安全的视点说,彻底可以通过传统的串行行列、锁等机制保证。
此时,可以通过 @unchecked
attribute 奉告编译器不进行 Sendable
语义检查,如:
// 相当于说 User 的并发安全由开发人员自行保证,不必编译器检查
class User: @unchecked Sendable {
var name: String
var age: Int
}
Sendable
作为协议只能用于常规类型,关于函数、闭包等则无能为力。
此时,就轮到 @Sendable
上台了。
@Sendable
被 @Sendable
修饰的函数、闭包可以跨 actor 传递。
关于前文提到的比方:
extension BankAccount {
func addAge(amount: Int, completion: (Int) -> Void) {
age += amount
completion(age)
}
}
extension AccountManager {
func addAge() async {
// Wraning: Non-sendable type '(Int) -> Void' passed in implicitly asynchronous call to actor-isolated instance method 'addAge(amount:completion:)' cannot cross actor boundary
await bankAccount.addAge(amount: 1, completion: { age in
print(age)
})
}
}
只需对 addAge
方法的 completion
参数加上 @Sendable
即可:
func addAge(amount: Int, completion: @Sendable (User) -> Void)
总结一下,用@Sendable
修饰 Closure 真正意味着什么?
其实是奉告 Closure 的完结者,该 Closure 或许会在并发环境下调用,请注意数据安全!
因而,假设对外提供的接口触及 Closure (作为方法参数、返回值),且其或许在并发环境下实行,就使用 @Sendable
修饰。
根据这一准则,actor 对外的方法如触及 Closure,也使用
@Sendable
修饰。
extension Task where Failure == Error {
public init(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success)
}
如 Task
的 operation
闭包会在并发环境下实行,故用了 @Sendable
修饰。
当然编译器会对 @Sendable Closure 的完结进行各种合规检查:
-
不能捕获 actor-isolated 特色,否则 Error: Actor-isolated property ‘x’ can not be referenced from a Sendable closure;(原因也很简单,@Sendable Closure 或许会在并发环境下实行,这与 actor 串行维护数据有冲突)
假设 @Sendable 闭包是异步的 (@Sendable () async ),则不受此束缚。
我们可以考虑一下是为啥?
-
不能捕获
var
变量,否则 Error: Mutation of captured var ‘x’ in concurrently-executing code; -
所捕获目标有必要完结 Sendable 协议,否则 Warning: Capture of ‘x’ with non-sendable type ‘X’ in a
@Sendable
closure。
还记得 Swift 新并发结构之 actor 中最后那个 crash 吗:
extension User {
func testUser(callback: @escaping () -> Void) {
for _ in 0..<1000 {
DispatchQueue.global().async {
callback()
}
}
}
}
extension BankAccount {
func test() {
let user = User.init(name: "Tom", age: 18)
user.testUser {
let b = self.balances[1] ?? 0.0
self.balances[1] = b + 1
print("i = \(0), \(Thread.current), balance = \(String(describing: self.balances[1]))")
}
}
}
这个 crash 该怎样 fix 呢?
可以考虑一下~
extension User {
// 因为 callback 会在并发环境下实行,故用 `@Sendable` 修饰
// 一般情况下,@Sendable closure 都是异步的,否则受限于 @Sendable 的规则无法捕获 Actor-isolated property
func test(callback: @escaping @Sendable () async -> Void) {
for _ in 0..<1000 {
DispatchQueue.global().async {
// 在同步上下文中一般通过 Task 打开一个异步上下文
Task{
await callback()
}
}
}
}
}
extension BankAccount {
func changeBalances(newValue: Double) {
balances[1] = newValue
}
func test() {
let user = User.init(name: "Tom", age: 18)
user.test { [weak self] in
guard let self = self else { return }
let b = await self.balances[1] ?? 0.0
// 对 Actor-isolated property 的修正需提取到独自的方法里
// 不能直接在 @Sendable 闭包修正
await self.changeBalances(newValue: b + 1)
print("i = \(0), \(Thread.current), balance = \(String(describing: await self.balances[1]))")
}
}
}
Future Improvement
Apple 在 Protect mutable state with Swift actors – WWDC21 上提到将来 Swift 编译器会制止同享 (传递) 非 Sendable 类型的实例。
那么,本文提到的全部 Warning 都将变成 Error!
好了,关于 Sendable 就聊这么多!
小结
-
Sendable
自身是一个 Marker Protocol,用于编译期的合规检查; -
全部值语义类型都主动遵循
Sendable
协议; -
全部遵循
Sendable
协议的类型都可以跨 actor 传递; -
@Sendable
用于修饰方法、闭包; -
关于会在并发环境下实行的闭包都使用
@Sendable
修饰。
参考资料
swift-evolution/0296-async-await.md at main apple/swift-evolution GitHub
swift-evolution/0302-concurrent-value-and-concurrent-closures.md at main apple/swift-evolution GitHub
swift-evolution/0337-support-incremental-migration-to-concurrency-checking.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
swift-evolution/0337-support-incremental-migration-to-concurrency-checking.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