背景:为什么需要 Swift Concurrency?
在 SwiftUI 出现之前,我们用 GCD(Grand Central Dispatch)做线程切换;
Swift 5.5 引入的 Actor、async/await 和 Task 把并发提升到了语言层面;
Swift 6.0 又带来了 Strict Concurrency 和数据竞争检查。
2025 年的 Swift 6.2(WWDC25) 再次出手,用两个开关把并发门槛降到"新手友好"级别:
新特性 | 作用 |
---|---|
Default Actor Isolation = MainActor | 所有自定义类型默认运行在主线程(MainActor),天生具备线程安全性 |
nonisolated nonsending by default = ON | nonisolated 方法自动继承调用者的执行线程,无需手动标记线程上下文(如 @MainActor 或 @AnyActor ) |
一句话总结:"先跑主线程,再按需后台,用 @concurrent
标记真正耗时的任务。"
回顾:async/await 与 Actor 基础
async / await 最小示例
swift
class Model {
func fetch() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
}
async
表示方法会 挂起(suspend)。await
是 挂起点,把线程让出来,等待结果返回后继续执行。
在 Swift 6.2 之前,没有隔离的
async
方法会随机跑在后台线程;从 6.2 开始,默认跑主线程,只有真正 I/O 的苹果 API(如 URLSession)才会切后台。
显式隔离:@MainActor & actor
swift
@MainActor // 整类跑主线程
@Observable
class WeatherVM {
var forecast: Forecast?
func reload() async throws {
forecast = try await service.load()
}
nonisolated func heavy() async { ... } // 显式脱离主线程
}
actor ImageCache { // 自定义隔离域,跑后台
private var store: [URL: Image] = [:]
func image(for url: URL) -> Image? { store[url] }
}
@MainActor
等价于"UI 专属线程"。actor
自带互斥锁,适合可变共享状态(如缓存)。nonisolated
让某个方法脱离当前 Actor,继承调用者线程。
Swift 6.2 的两把"默认开关"
构建设置 | Xcode 26 默认值 | Apple 推荐值 |
---|---|---|
Default Actor Isolation | MainActor | MainActor |
nonisolated nonsending by default | OFF | ON |
开启后:
- 所有新建类型 自动 @MainActor,无需手写。
- nonisolated 方法 默认 (nonsending),即 继承调用者线程。
- 只有用 @concurrent 宏标记的方法,才 强制跑后台。
swift
// 默认 MainActor
class Model {
func businessLogic() async { ... } // 主线程
nonisolated
func helper() async { ... } // 继承调用者线程
@concurrent
func cpuIntensive() async { ... } // 必跑后台
}
实战:线程流向分析题
题目:下面的 ConcurrentThread
各方法分别跑在哪条线程?
swift
struct MainMaster: View {
var body: some View {
VStack { Text("Main Master") }
.task {
Task.detached {
let model = ConcurrentThread()
await model.firstMethod()
}
}
}
}
class ConcurrentThread {
func firstMethod() async { // ①
await secondMethod()
await thirdMethod()
}
@concurrent
func secondMethod() async { // ②
try? await Task.sleep(for: .seconds(1))
await thirdMethod()
}
nonisolated
func thirdMethod() async { ... } // ③
}
4.1 执行顺序与线程
步骤 | 方法 | 线程 | 原因 |
---|---|---|---|
① | firstMethod() |
Main | 类默认 @MainActor ;Task.detached 不影响隔离上下文 |
② | secondMethod() |
后台 | 标记 @concurrent ,强制方法在后台 Actor 运行 |
③-1 | thirdMethod() |
后台 | 由 ② 调用,且方法标记为 nonisolated(nonsending) ,继承调用者(后台)的线程上下文 |
③-2 | thirdMethod() |
Main | 由 ① 调用,继承调用者(MainActor)的线程上下文,因此运行在主线程 |
结论:
await
只负责挂起和恢复;@concurrent
和nonisolated
才是决定线程的关键。
Closure Capture:@Sendable vs sending
Swift 6 用 sending
取代 @Sendable
,但概念不变:闭包跨线程时必须保证捕获的值是 线程安全 的。
经典错误
swift
class MapManager { var response: MKLocalSearch.Response? }
class MapModel {
let manager = MapManager() // 非 Sendable
func getResponse() {
Task { // 隐式 @Sendable
manager.response = ... // ❌ 捕获 self,产生数据竞争
}
}
}
正确姿势
- 使用局部变量
- 或使用
@unchecked Sendable
(仅当你真的知道安全) - 或把类型标记
@MainActor
,此时编译器自动视为 Sendable
常见坑:@Sendable 同步闭包
SwiftUI 部分 API(如 PhotosPicker
)的 label
仍是 同步 @Sendable 闭包,内部不能调用 @MainActor
方法。
swift
PhotosPicker(selection: $model.pickerItem) {
if let image = model.imageDataToUI(cocktail) { // ❌
Image(uiImage: image)
}
}
解决
把值提前算好,再塞进闭包:
swift
var body: some View {
let image = model.imageDataToUI(cocktail)
PhotosPicker(selection: $model.pickerItem) {
if let image { Image(uiImage: image) }
}
}
性能优化:只在需要时用 @concurrent
在 Structured Concurrency 里,子任务默认就在后台,不需要 @concurrent
:
swift
await withTaskGroup(of: Void.self) { group in
for album in albums {
group.addTask { // 已自动后台
await self.process(album)
}
}
}
但如果 process(album)
内部还有 大量 CPU 计算,再在 process
上加 @concurrent
:
swift
@concurrent
func process(_ album: Album) async { ... }
Instruments 前后对比:主线程瞬间从 100 % 降到 10 %。
总结:Apple 推荐的并发范式
阶段 | 做法 | 备注 |
---|---|---|
默认 | 开启 Default Actor Isolation = MainActor | 90% 代码直接跑主线程 |
需要后台 | 在方法上加 @concurrent |
明确告诉编译器"这里耗时" |
共享可变状态 | 用 actor |
自动加锁保证状态安全 |
跨线程闭包 | 用局部变量或 Sendable | 避免闭包捕获 self 导致线程安全问题 |
一句话:"先跑主线程,再按需后台,用 @concurrent 标记真正耗时的任务。"