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闭包要求捕获的所有值都是 SendableTask.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
- Visualize and optimize Swift concurrency — learn how you can optimize your app with the swift concurrency template in instruments.
- Swift concurrency: Behind the scenes — dive deep into the swift concurrency runtime and learn how async/await and actors work under the hood.
- Meet async/await in Swift — learn how to use async/await to write asynchronous code that is easier to read and maintain.
评论
GitHub Issues · utterances