Swift并发Sendability错误困惑:为何部分案例编译结果不同?
先定义测试用的非Sendable类型:
class Foo { var foo = 3 init() {} }
所有测试代码均在viewDidLoad方法中执行。
基础测试案例
案例1:编译正常
DispatchQueue.global().async { Task { let foo = Foo() Task { @MainActor in print(foo) } } }
案例2:编译报错
let foo = Foo() DispatchQueue.global().async { Task { Task { @MainActor in print(foo) } } }
案例3:编译报错
DispatchQueue.global().async { let foo = Foo() Task { Task { @MainActor in print(foo) } } }
核心疑问
原本预期所有案例都会编译失败,但案例1能通过编译,案例1与案例2、3的差异是什么?
补充测试案例
案例4:仅触发警告而非错误
let foo = Foo() DispatchQueue.global().async { Task { @MainActor in print(foo) } }
警告信息翻译:
捕获的变量
foo不可Sendable,存在并发数据竞争风险。在非Sendable类型上禁用并发检查可使用@unchecked Sendable,但这会绕过Swift的安全检查。
案例5:编译正常
DispatchQueue.global().async { let foo = Foo() Task { @MainActor in print(foo) } }
补充疑问
为何案例4仅触发警告而非错误?案例5又为何能编译正常?
原因解析
案例1通过编译的核心原因
案例1中,Foo实例是在全局队列启动的根Task内部创建的,这个Task属于未指定Actor的并发上下文,后续@MainActor的子Task直接继承该根Task的上下文。Swift并发检查认为:该Foo实例从创建到被@MainActorTask访问,全程处于同一个隐式非Actor并发域中,没有跨真正的Actor边界传递,因此豁免了Sendable检查。
案例2、3报错的原因
- 案例2:
Foo实例创建在viewDidLoad的Main Actor上下文中,随后被捕获到全局队列闭包,再传递到@MainActor的Task。这里存在两次跨上下文传递:从Main Actor到全局队列非Actor上下文,再回到Main Actor,两次跨边界都要求Foo必须是Sendable类型,因此直接报错。 - 案例3:
Foo实例创建在全局队列闭包(非Actor上下文)中,被捕获到外层Task后传递到@MainActor的子Task。外层Task与内层@MainActorTask属于不同的并发上下文,传递非Sendable实例触发严格的Sendable检查,因此报错。
案例4仅触发警告的原因
案例4中,Foo实例从Main Actor上下文被捕获到全局队列闭包,再直接传递到@MainActor的Task。Swift检查器识别到最终回到了原Actor(Main Actor),因此降低检查等级,仅触发警告——但仍提示数据竞争风险,因为全局队列闭包阶段,Foo实例可能被其他线程访问。
案例5编译正常的原因
案例5中,Foo实例创建在全局队列闭包(非Actor上下文)中,直接传递到@MainActor的Task。这种从“非Actor并发域”到Main Actor的一次性传递,Swift检查器认为没有经过其他Actor上下文,因此豁免了Sendable检查,属于对简单跨上下文传递的宽松处理。
内容的提问来源于stack exchange,提问作者OMGPOP

