Eliminate data races using Swift Concurrency
Swift & UI 进阶 20m

使用 Swift 并发消除数据竞争

Eliminate data races using Swift Concurrency

2022年6月6日

在 Apple 官方观看视频

一句话判断

Doug Gregor 用航海比喻系统性地讲解了 Swift 并发模型如何通过隔离(Isolation)和 Sendable 协议在编译期防止数据竞争——这是理解 Swift 6 严格并发检查的基础。

这场 Session 讲了什么

Swift 团队的 Doug Gregor 从一个高层视角解释了 Swift 并发消除数据竞争的整体设计,而不是聚焦于某个具体的语言特性。

核心模型:并发中的任务是相互独立的”船”,每条船有自己的资源。船之间传递数据时,需要”海关检查员”(Swift 编译器)确保传递的数据是安全的。值类型(如 struct)传副本,天然安全;引用类型(如 class)传引用,会导致共享可变状态,产生数据竞争。

Sendable 协议是关键机制——只有满足 Sendable 的类型才能跨越隔离边界。编译器在 task 创建、闭包传递、actor 消息发送等多个检查点验证 Sendable 约束。Actor 提供了隔离的可变状态,通过异步消息传递安全地共享数据。

值得深挖的点

值类型 vs 引用类型在并发中的本质区别。 Pineapple(struct)传副本,修改不影响原件;Chicken(class)传引用,两边都能改同一个对象。这个区别在单线程代码中不那么重要,在并发中却是数据竞争的根源。Sendable 协议在编译期帮你区分这两者。

Sendable 的一致性规则。 Struct 和 enum 只要所有存储属性都是 Sendable 就自动满足(非 public 类型可以由编译器推断)。Class 要成为 Sendable 非常严格——必须是 final 且只有不可变存储,或者用 @unchecked Sendable 放弃编译器检查。后者意味着你自己承担正确性的责任。

Actor 的隔离模型。 Actor 内部的状态只能通过 actor 的方法访问,外部调用必须用 await,这实际上是一次异步消息传递。这种设计保证了 actor 的可变状态同一时刻只有一个任务在访问。

代码片段

// Sendable 类型的编译器检查
struct Pineapple: Sendable {
    var weight: Double
    var ripeness: Int
}

// 非 Sendable 类型
class Chicken {
    var name: String
    var isHungry: Bool
}

// 编译器阻止跨隔离边界传递非 Sendable 类型
func shareWithFriend() async {
    let pineapple = Pineapple(weight: 1.2, ripeness: 5)
    let chicken = Chicken(name: "Clucky", isHungry: true)

    // 通过:Pineapple 是 Sendable
    let task1 = Task { pineapple }

    // 编译错误:Chicken 不是 Sendable,不能跨 task 边界传递
    // let task2 = Task { chicken }
}
// Actor 提供隔离的可变状态
actor Coop {
    private var chickens: [Chicken] = []

    func add(_ chicken: Chicken) {
        chickens.append(chicken)
    }

    func feedAll() {
        for chicken in chickens {
            chicken.isHungry = false
        }
    }
}

// 外部访问必须用 await(异步消息传递)
let coop = Coop()
await coop.add(Chicken(name: "Clucky", isHungry: true))
await coop.feedAll()

最佳实践

  • 让数据类型尽量满足 Sendable。 优先使用值类型(struct/enum),如果必须用 class,确保它是不可变的或自己做了内部同步。
  • 谨慎使用 @unchecked Sendable 它绕过了编译器检查,如果你的内部同步有漏洞,数据竞争还是会发生。
  • 理解 Sendable 检查点。 Task 创建、@Sendable 闭包、actor 消息传递——这些是编译器强制检查的边界。
  • 从架构层面考虑并发。 Swift 并发不是一个局部优化,而是影响整体代码结构的编程模型。新项目应该从设计阶段就考虑隔离边界。

还有什么值得关注

  • 推荐观看 2021 年的 Swift Concurrency 系列 Sessions 了解各个语言特性的细节
  • Swift 6 的严格并发检查将进一步强化 Sendable 约束
  • @Sendable 闭包标注暗示闭包可能在不同隔离域执行,捕获的值必须是 Sendable 的
  • nonisolated 关键字可以让 actor 的某些方法不参与隔离,用于纯计算场景
WWDC 2022