WWDC Quick Look 💓 By SwiftGGTeam
Eliminate data races using Swift Concurrency

Eliminate data races using Swift Concurrency

观看原视频

Highlight

Swift Concurrency 通过 Task 隔离、Sendable 协议和 Actor 三种机制消除数据竞争。Doug 用”海上船只”的类比解释了值类型和引用类型在并发中的区别,演示了编译器如何在 Task 边界和 Actor 边界检查 Sendable 合规性,并讨论了原子性、主线程隔离和严格并发检查模式等进阶话题。

核心内容

Swift 5.5 引入了 async/await、Task 和 Actor,让并发代码更容易写对。这场 session 从更高层面解释这些机制如何配合消除数据竞争,以及它们对 App 架构的影响。(00:10

Doug 用了一个贯穿全场的类比:Task 是海上的船,各自独立航行;Actor 是海上的岛,有自己的状态,一次只能接待一艘船;Sendable 是海关检查,确保船与船之间、船与岛之间传递的东西是安全的。(01:17

值类型(struct、enum)天然适合并发共享,因为传递时是复制,各有一份独立数据。引用类型(class)传递的是引用,多个 Task 同时修改同一份数据就会产生数据竞争。Swift 用 Sendable 协议来标记哪些类型可以安全地跨隔离域传递:值类型自动符合 Sendable(只要成员都是 Sendable),引用类型只有在不可变或自己做同步时才能符合。(02:31

Actor 是共享可变状态的安全方式。Actor 的所有属性和方法默认隔离在 Actor 内部,外部访问必须通过 await。同一时间只有一个 Task 能在 Actor 上执行,这消除了数据竞争。Actor 本身隐式符合 Sendable,因为引用一个 Actor 只是拿到一张”地图”,真正访问状态还需要通过隔离检查。(10:28

主线程对应 Main Actor,所有 UI 操作必须在上面执行。Main Actor 也是 Actor,所以一次只能执行一个任务,长时间占用会导致 UI 卡死。正确的架构是把视图和控制器放在 Main Actor,业务逻辑分离到其他 Actor 或普通 Task。(16:04

原子性是另一个关键话题。Actor 保证同一时间只有一个任务执行,但 await 是挂起点,挂起期间 Actor 可以执行其他任务。如果在两个 await 之间做了状态假设,可能出现高级别数据竞争(状态不一致但无数据损坏)。解决方案是把需要原子性的操作写成 Actor 内的同步方法。(19:08

Swift 5.7 引入了严格并发检查模式,分三级:Minimal(只检查显式标记 Sendable 的地方)、Targeted(检查已采用并发特性的代码)、Complete(检查所有代码,接近 Swift 6 语义)。建议逐步启用,用 @preconcurrency import 暂时抑制外部模块的警告。(24:36

详细内容

Task 隔离与 Sendable 协议

01:17)Task 是并发中的基本工作单元,每个 Task 有自己的资源,独立执行。但 Task 之间需要通信,这就涉及数据共享。

值类型传递时是复制:

enum Ripeness {
    case hard
    case perfect
    case mushy(daysPast: Int)
}

struct Pineapple {
    var weight: Double
    var ripeness: Ripeness
    
    mutating func ripen() async { ... }
    mutating func slice() -> Int { ... }
}

关键点:

  • Pineapple 是 struct,传递时复制
  • 每个 Task 拿到自己的副本,修改互不影响
  • Swift 编译器自动推断 Pineapple: Sendable,因为所有成员都是 Sendable

引用类型传递的是引用:

final class Chicken {
    let name: String
    var currentHunger: HungerLevel
    
    func feed() { ... }
    func play() { ... }
    func produce() -> Egg { ... }
}

关键点:

  • Chicken 是 class,传递时复制引用
  • 两个 Task 引用同一个对象,同时修改会产生数据竞争
  • Chicken 不能符合 Sendable,因为它有可变状态

Sendable 协议用于标记可安全跨隔离域传递的类型:

protocol Sendable { }

struct Pineapple: Sendable { ... } // 自动符合
class Chicken: Sendable { } // 编译错误:未同步的引用类型

编译器在 Task 边界检查 Sendable:

// 编译错误:Chicken 不是 Sendable
let petAdoption = Task {
    let chickens = await hatchNewFlock()
    return chickens.randomElement()!
}
let pet = await petAdoption.value

关键点:

  • Task 的泛型参数 Success: Sendable 要求返回值必须是 Sendable
  • 这个约束来自 Task 的定义:struct Task<Success: Sendable, Failure: Error>
  • Sendable 约束也应该用在你的泛型参数上,如果值会跨隔离域传递

集合类型的 Sendable 通过条件一致性传播:

// 符合 Sendable:成员都是 Sendable
struct Crate: Sendable {
    var pineapples: [Pineapple]
}

// 编译错误:Chicken 不是 Sendable
struct Coop: Sendable {
    var flock: [Chicken]
}

类要符合 Sendable 需要满足严格条件:

// 编译错误:currentHunger 是可变的
final class Chicken: Sendable {
    let name: String
    var currentHunger: HungerLevel
}

// 只有不可变成员的 final class 可以符合 Sendable
final class Egg: Sendable {
    let color: EggColor
    let weight: Double
}

对于自己做内部同步的引用类型,可以用 @unchecked Sendable,但要小心:

class ConcurrentCache<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable {
    var lock: NSLock
    var storage: [Key: Value]
}

关键点:

  • @unchecked Sendable 禁用编译器检查
  • 只有在确实做了正确同步时才使用
  • 滥用会破坏 Swift 的数据竞争安全保证

Task 创建时的闭包也有 Sendable 检查:

let lily = Chicken(name: "Lily")
Task.detached { @Sendable in
    lily.feed() // 编译错误:捕获了非 Sendable 的 lily
}

关键点:

  • @Sendable 闭包要求捕获的所有值都是 Sendable
  • Task.detached 的 operation 参数类型就是 @Sendable 闭包
  • 这确保新 Task 不会引入数据竞争

Actor 隔离与状态保护

10:28)Actor 提供了一种安全共享可变状态的方式:

actor Island {
    var flock: [Chicken]
    var food: [Pineapple]

    func advanceTime()
}

关键点:

  • Actor 的实例属性和实例方法默认隔离在该 Actor 上
  • 外部访问必须通过 await,这是一个潜在的挂起点
  • 同一时间只有一个 Task 能在 Actor 上执行
  • Actor 隐式符合 Sendable

调用 Actor 方法:

func nextRound(islands: [Island]) async {
    for island in islands {
        await island.advanceTime()
    }
}

关键点:

  • 每个 await island.advanceTime() 都是一次”登岛”
  • 登岛期间可以访问该 Actor 的所有状态
  • 离开岛(await 结束)后,其他 Task 可以登岛

非 Sendable 数据不能在 Task 和 Actor 之间共享:

// 两个都编译错误
await myIsland.addToFlock(myChicken) // 把非 Sendable 的 chicken 传给 Actor
myChicken = await myIsland.adoptPet() // 从 Actor 取出非 Sendable 的 chicken

Actor 隔离的代码范围:

actor Island {
    var flock: [Chicken]
    var food: [Pineapple]

    func advanceTime() {
        // 闭包不标记 @Sendable,继承 Actor 隔离
        let totalSlices = food.indices.reduce(0) { (total, nextIndex) in
            total + food[nextIndex].slice()
        }

        // Task 继承创建时的 Actor 隔离
        Task {
            flock.map(Chicken.produce)
        }

        // Task.detached 不继承 Actor 隔离
        Task.detached {
            let ripePineapples = await food.filter { $0.ripeness == .perfect }
            print("There are \(ripePineapples.count) ripe pineapples on the island")
        }
    }
}

关键点:

  • @Sendable 闭包在 Actor 方法内继承 Actor 隔离
  • Task {} 继承创建时的 Actor 隔离上下文
  • Task.detached {} 是完全独立的,不继承任何 Actor 隔离

nonisolated 显式脱离 Actor 隔离:

extension Island {
    nonisolated func meetTheFlock() async {
        let flockNames = await flock.map { $0.name }
        print("Meet our fabulous flock: \(flockNames)")
    }
}

关键点:

  • nonisolated 让方法在 Actor 外部执行
  • 访问 Actor 状态仍需 await
  • 适合不需要频繁访问 Actor 状态的辅助方法

Main Actor 与 App 架构

16:04)Main Actor 代表主线程,所有 UI 操作必须在其上执行:

@MainActor func updateView() { ... }

Task { @MainActor in
    view.selectedChicken = lily
}

nonisolated func computeAndUpdate() async {
    computeNewValues()
    await updateView()
}

关键点:

  • @MainActor 可以标记函数、闭包或类型
  • 从非 Main Actor 上下文调用需要 await
  • 长时间占用 Main Actor 会导致 UI 卡死

Main Actor 也可以标记整个类型:

@MainActor
class ChickenValley: Sendable {
    var flock: [Chicken]
    var food: [Pineapple]

    func advanceTime() {
        for chicken in flock {
            chicken.eat(from: &food)
        }
    }
}

关键点:

  • 类型的所有属性和方法默认隔离到 Main Actor
  • Main Actor 类型的引用是 Sendable 的,因为数据被隔离
  • 适合视图、视图控制器等必须跑在主线程的类型

对 App 架构的影响:

  • 视图和视图控制器在 Main Actor 上
  • 业务逻辑分离到其他 Actor 或普通 Task
  • Task 在 Main Actor 和其他 Actor 之间按需穿梭

原子性与高级别数据竞争

19:08)Actor 消除了低级别数据竞争(数据损坏),但高级别数据竞争(状态不一致)仍可能发生:

// 有问题:两个 await 之间状态可能变化
func deposit(pineapples: [Pineapple], onto island: Island) async {
    var food = await island.food // await 1:拿到副本
    food += pineapples // 在本地修改
    await island.food = food // await 2:写回去,但期间可能已被修改
}

关键点:

  • 两个 await 之间,Actor 可以执行其他任务
  • 如果其他任务修改了 food,最后的赋值会覆盖那些修改
  • 这是高级别数据竞争:没有数据损坏,但逻辑错误

正确做法:把操作写成 Actor 内的同步方法:

extension Island {
    func deposit(pineapples: [Pineapple]) {
        var food = self.food
        food += pineapples
        self.food = food
    }
}

关键点:

  • 同步方法在 Actor 上不可分割地执行
  • 没有 await,不会插入其他任务
  • 设计 Actor 时,把需要原子性的操作做成同步方法

严格并发检查模式

24:36)Swift 5.7 引入三级严格并发检查:

Minimal(默认):

  • 只检查显式标记 Sendable 的地方
  • 和 Swift 5.5/5.6 行为类似
import FarmAnimals
struct Coop: Sendable {
    var flock: [Chicken] // 警告:Chicken 不是 Sendable
}

Targeted:

  • 检查已采用并发特性(async/await、Task、Actor)的代码
  • 对未采用并发的代码保持兼容
@preconcurrency import FarmAnimals

func visit(coop: Coop) async {
    guard let favorite = coop.flock.randomElement() else {
        return
    }
    Task {
        favorite.play()
    }
}

关键点:

  • @preconcurrency import 暂时抑制外部模块的 Sendable 警告
  • 当外部模块更新后,编译器会重新检查
  • 适合逐步迁移的场景

Complete:

  • 检查模块中所有代码
  • 接近 Swift 6 的预期语义
import FarmAnimals

func doWork(_ body: @Sendable @escaping () -> Void) {
    DispatchQueue.global().async {
        body()
    }
}

func visit(friend: Chicken) {
    doWork {
        friend.play() // 警告:捕获了非 Sendable 的 Chicken
    }
}

关键点:

  • Complete 模式会找出所有潜在的数据竞争
  • 建议逐步启用:先 Targeted,再 Complete
  • @preconcurrency 处理尚未更新的依赖模块

核心启发

  • 做什么:审查项目中的 class,把可变状态改成 struct 或 Actor。
    为什么值得做:Doug 用 Pineapple(struct)和 Chicken(class)的对比说明,值类型天然适合并发,因为传递时复制。class 的共享引用是数据竞争的根源。Swift 编译器自动推断 struct/enum 的 Sendable,但 class 需要严格条件。
    怎么开始:遍历项目中的数据模型,问自己”这个类型真的需要引用语义吗?“如果不需要,改成 struct。如果需要共享可变状态,改成 Actor。

  • 做什么:启用 Targeted 严格并发检查,修复 Sendable 警告。
    为什么值得做:Swift 5.7 的 Targeted 模式只检查已采用并发特性的代码,不会破坏现有代码。修复这些警告可以消除大部分潜在的数据竞争。Doug 建议逐步走向 Complete 检查,为 Swift 6 做准备。
    怎么开始:在 Build Settings 里把 “Strict Concurrency Checking” 改成 Targeted,编译项目,逐个修复 Sendable 相关警告。对外部模块用 @preconcurrency import 暂时抑制。

  • 做什么:检查 Actor 中的异步方法,把需要原子性的操作改成同步方法。
    为什么值得做:Doug 的 deposit 例子说明,两个 await 之间 Actor 状态可能变化。把多步操作封装成 Actor 内的同步方法,可以确保原子性。
    怎么开始:审查所有 Actor 的 public 方法,找出包含多个 await 的方法。问自己”如果在这两个 await 之间插入了其他任务,状态还正确吗?“如果不确定,把逻辑改成同步方法。

关联 Session

评论

GitHub Issues · utterances