深入理解 Swift 6.2 并发:从默认隔离到@concurrent 的完整指南

原文:QMastering Swift 6.2 Concurrency -- A Complete Tutorial

背景:为什么需要 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

开启后:

  1. 所有新建类型 自动 @MainActor,无需手写。
  2. nonisolated 方法 默认 (nonsending),即 继承调用者线程。
  3. 只有用 @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 类默认 @MainActorTask.detached不影响隔离上下文
secondMethod() 后台 标记 @concurrent,强制方法在后台 Actor 运行
③-1 thirdMethod() 后台 由 ② 调用,且方法标记为 nonisolated(nonsending),继承调用者(后台)的线程上下文
③-2 thirdMethod() Main 由 ① 调用,继承调用者(MainActor)的线程上下文,因此运行在主线程

结论:await 只负责挂起和恢复;@concurrentnonisolated 才是决定线程的关键。

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 标记真正耗时的任务。"

相关推荐
HarderCoder24 分钟前
我们真的需要 typealias 吗?——一次 Swift 抽象成本的深度剖析
swift
HarderCoder1 小时前
ByAI-Swift 6 全览:一份面向实战开发者的新特性速查手册
swift
HarderCoder2 小时前
Swift 中 let 与 var 的真正区别:不仅关乎“可变”与否
swift
麦兜*21 小时前
Swift + Xcode 开发环境搭建终极指南
开发语言·ios·swiftui·xcode·swift·苹果vision pro·swift5.6.3
HarderCoder2 天前
Swift Concurrency:彻底告别“线程思维”,拥抱 Task 的世界
swift
HarderCoder2 天前
深入理解 Swift 中的 async/await:告别回调地狱,拥抱结构化并发
swift
HarderCoder3 天前
深入理解 SwiftUI 的 ViewBuilder:从隐式语法到自定义容器
swiftui·swift
HarderCoder3 天前
在 async/throwing 场景下优雅地使用 Swift 的 defer 关键字
swift
东坡肘子3 天前
我差点失去了巴顿(我的狗狗) | 肘子的 Swift 周报 #098
swiftui·swift·apple