Swift 6.2 Approachable Concurrency 完全指南:MainActor 默认隔离与 @concurrent 实战

Swift 6.2 的 Approachable Concurrency 通过 MainActor 默认隔离、nonisolated(nonsending) 和 @concurrent 三大改变,让并发编程变成了你主动选择的事情。从概念到代码,一篇搞定迁移。

说实话,如果你从 Swift 5 一路用过来,对并发的感受大概率就是两个字——头疼。async/await 本身还好,语法清晰、上手也快。可一旦你鼓起勇气开启 Swift 6 的严格并发检查,编译器就像突然翻脸了一样:满屏的 Sendable 错误、Actor 隔离冲突、莫名其妙的"跨隔离域"警告……我见过不少团队卡在这一步,迟迟不敢真正切到 Swift 6。

Apple 显然也意识到了这个问题。

于是在 Swift 6.2(随 Xcode 26 一起发布)中,他们推出了一套叫做 Approachable Concurrency 的改进方案。核心思路其实很简单:让并发变成你主动选择的东西,而不是编译器强塞给你的。我个人觉得这个方向终于对了。

这篇文章会把 Swift 6.2 的三个核心并发变化讲透——MainActor 默认隔离nonisolated(nonsending) 默认行为、以及新的 @concurrent 注解。每个概念都配了可运行的代码示例,看完你就能在自己项目里放心启用这些特性了。

为什么需要 Approachable Concurrency?

先回顾一下痛点吧。在 Swift 6.0/6.1 中,如果你开启了严格并发检查,下面这种日常场景就能让你抓狂:

// Swift 6.0 — 这段代码会报错
class UserManager {
    var currentUser: User? // ⚠️ 全局变量不是并发安全的

    func fetchUser() async -> User {
        let data = try await URLSession.shared.data(from: url)
        let user = try JSONDecoder().decode(User.self, from: data.0)
        currentUser = user // ⚠️ 从非隔离的异步上下文修改属性
        return user
    }
}

就这么一个简单的网络请求,编译器能给你抛出一堆错误:currentUser 不是 Sendable 的、异步函数运行在全局执行器上导致跨隔离域访问……你明明只是想发个网络请求而已,却被迫先上一堂并发理论课。

Approachable Concurrency 的设计哲学是渐进式披露(Progressive Disclosure),说白了就是:

  1. 先写顺序代码——默认一切运行在 MainActor 上,不需要任何并发注解
  2. 需要异步时加 async/await——但函数仍然留在调用者的隔离域内
  3. 真正需要并行时才引入并发——用 @concurrent 明确标记需要后台执行的函数

这意味着大多数 iOS 开发者在日常开发中根本不需要接触复杂的并发概念。只有当你确实需要把重活丢到后台(比如图片处理、大批量 JSON 解析)的时候,才需要显式地"选择进入"并发世界。这个思路,对吧?

核心变化一:MainActor 默认隔离(SE-0466)

这是 Swift 6.2 最大的一个改变,也是我觉得最实用的一个:你的代码默认运行在 MainActor 上,除非你明确说不需要。

之前 vs 现在

在 Swift 6.0/6.1 中,代码默认是 nonisolated 的——也就是没有隔离到任何 Actor。这导致你需要手动给大量 UI 相关代码加上 @MainActor

// Swift 6.0 — 需要到处标注 @MainActor
@MainActor
class ProfileViewModel: ObservableObject {
    @MainActor @Published var userName: String = ""
    @MainActor @Published var isLoading: Bool = false

    @MainActor
    func loadProfile() async {
        isLoading = true
        // ...
    }
}

看着就累,是不是?

在 Swift 6.2 开启 MainActor 默认隔离后:

// Swift 6.2 — 默认就在 MainActor 上,干净多了
class ProfileViewModel: ObservableObject {
    @Published var userName: String = ""
    @Published var isLoading: Bool = false

    func loadProfile() async {
        isLoading = true
        // ...
    }
}

没有任何 @MainActor 注解,但编译器知道一切都运行在主线程上。这跟大多数 iOS 应用的实际运行方式完全一致——你的 ViewModel、Service、Manager 大部分时间本来就应该待在主线程上。

需要退出 MainActor 隔离时怎么办?

有些代码确实不需要绑在 MainActor 上,这时候用 nonisolated 标记就行:

// 模型层通常不需要绑定到任何 Actor
nonisolated struct UserProfile: Codable, Sendable {
    let id: UUID
    let name: String
    let email: String
}

// 纯计算函数也不需要
nonisolated func calculateHash(for data: Data) -> String {
    data.base64EncodedString()
}

思路特别清晰:默认安全(MainActor),按需退出(nonisolated)。比以前那套"默认裸奔(nonisolated),到处加锁(@MainActor)"好太多了。

核心变化二:nonisolated(nonsending) 默认行为(SE-0461)

这个变化解决了一个让很多人困惑已久的问题:nonisolated 异步函数到底运行在哪里?

Swift 6.0 的混乱行为

在 Swift 6.0 中,nonisolated 的同步函数和异步函数的行为是不一致的,这一点坑了不少人:

class DataProcessor {
    // 同步函数:运行在调用者的线程上(符合直觉)
    nonisolated func processSync() {
        // 如果从 MainActor 调用,就在主线程运行
    }

    // 异步函数:跳到全局执行器上!(完全不符合直觉)
    nonisolated func processAsync() async {
        // 不管从哪里调用,都会跑到后台线程
        // 这意味着你传入的参数必须是 Sendable 的
    }
}

异步函数会自动跳到全局后台执行器——这等于偷偷创建了一个新的隔离域。于是编译器要求所有传入参数必须是 Sendable 的,即使你根本没打算做任何并行操作。说实话,这个设计决策当初让我困惑了很久。

Swift 6.2 的统一行为

现在好了。nonisolated 异步函数的默认行为变成了 nonisolated(nonsending)——继承调用者的隔离上下文

// Swift 6.2 — nonisolated 异步函数默认继承调用者隔离
class DataProcessor {
    nonisolated func processAsync() async {
        // 从 MainActor 调用?就在 MainActor 上运行
        // 从某个自定义 Actor 调用?就在那个 Actor 上运行
        // 不再自动跳到后台线程
    }
}

// 实际使用
@MainActor
class ViewModel {
    let processor = DataProcessor()

    func doWork() async {
        // processAsync() 会在 MainActor 上运行
        // 不需要 Sendable,不会跨隔离域
        await processor.processAsync()
    }
}

好处一目了然:

  • 不再需要到处标 Sendable——因为根本没有跨隔离域
  • 行为与同步函数一致——少了一个需要记住的特殊规则
  • 代码更容易推理——你永远知道函数在哪里运行

显式标记 nonisolated(nonsending)

如果你的项目暂时还没启用 NonisolatedNonsendingByDefault 特性标志,也可以先对单个函数手动标记:

class NetworkService {
    // 手动标记:继承调用者的隔离上下文
    nonisolated(nonsending) func fetchData(from url: URL) async throws -> Data {
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    }
}

(这在渐进式迁移的时候特别有用。)

核心变化三:@concurrent 注解

OK,既然 nonisolated 异步函数不再自动跑到后台线程了,那当你真的需要在后台执行耗时操作时怎么办?

这就是 @concurrent 登场的时候了。

什么时候用 @concurrent

@concurrent 明确告诉编译器:"这个函数应该在全局并发执行器上运行,别留在调用者的 Actor 上。"

class ImageProcessor {
    // 这个函数做 CPU 密集型工作,应该在后台线程运行
    @concurrent
    func applyFilters(to image: UIImage) async -> UIImage {
        // 复杂的图片处理——不应该阻塞主线程
        let ciImage = CIImage(image: image)!
        let filter = CIFilter(name: "CIGaussianBlur")!
        filter.setValue(ciImage, forKey: kCIInputImageKey)
        filter.setValue(10.0, forKey: kCIInputRadiusKey)
        // ... 更多滤镜处理
        return processedImage
    }

    // 这个函数只是做网络请求,不需要后台线程
    // 默认就是 nonisolated(nonsending),继承调用者隔离
    nonisolated func fetchImageMetadata(id: String) async throws -> ImageMetadata {
        let url = URL(string: "https://api.example.com/images/\(id)")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(ImageMetadata.self, from: data)
    }
}

@concurrent vs nonisolated(nonsending) 决策指南

到底什么时候用哪个?其实判断标准很直接:

// ✅ 用 nonisolated(默认 nonsending):
// - 网络请求(大部分时间在等 I/O,不占 CPU)
// - 简单的数据转换
// - 不涉及大量计算的操作
nonisolated func fetchUser() async throws -> User { ... }

// ✅ 用 @concurrent:
// - 图片/视频处理
// - 大量 JSON 解析(比如解析几 MB 的数据)
// - 加密/哈希计算
// - 任何会阻塞线程超过几毫秒的 CPU 工作
@concurrent
func processLargeDataset(_ data: Data) async -> [Record] { ... }

记住一条经验法则就够了:如果操作主要是等待(I/O bound),用默认的 nonsending;如果操作主要是计算(CPU bound),用 @concurrent

推断隔离一致性(SE-0470)

Swift 6.2 还顺带解决了一个让人特别烦的小问题:推断隔离一致性(Inferred Isolated Conformances)

之前如果你有一个 @MainActor 类型想遵循 Equatable,你得写一堆别扭的 nonisolated 变通代码:

// Swift 6.0 — 痛苦的变通方案
@MainActor
class UserSettings {
    var theme: String = "light"
    var fontSize: Int = 14

    // 必须标记为 nonisolated 才能遵循 Equatable
    // 但这意味着从非主线程也能调用,破坏了隔离保证
    nonisolated static func == (lhs: UserSettings, rhs: UserSettings) -> Bool {
        // ⚠️ 这里访问 actor 隔离的属性其实不安全
        lhs.theme == rhs.theme && lhs.fontSize == rhs.fontSize
    }
}

说白了就是为了让编译器闭嘴,你不得不写出不安全的代码。这算什么事?

Swift 6.2 中,编译器终于聪明了——既然类型是 @MainActor 的,那它的协议遵循自然也应该在 MainActor 上下文中:

// Swift 6.2 — 编译器自动推断隔离一致性
@MainActor
class UserSettings: Equatable {
    var theme: String = "light"
    var fontSize: Int = 14

    static func == (lhs: UserSettings, rhs: UserSettings) -> Bool {
        // ✅ 编译器知道这个 conformance 仅在 MainActor 上有效
        lhs.theme == rhs.theme && lhs.fontSize == rhs.fontSize
    }
}

这个改进对 HashableComparableCustomStringConvertible 等常见协议都适用。终于不用为了遵循个协议就写不安全的代码了。

如何启用 Approachable Concurrency

好了,概念都讲完了,来看看具体怎么在项目里开启。

Xcode 26 新项目

如果你用 Xcode 26 创建新项目,恭喜——Approachable Concurrency 默认就是开启的。Build Settings 中会自动设置好:

  • Approachable Concurrency:Yes
  • Default Actor Isolation:MainActor

开箱即用,不需要做任何额外配置。

已有项目迁移

对于已有项目,你需要手动开启。在 Xcode 的 Build Settings 里操作:

  1. 搜索 "Approachable Concurrency",设为 Yes(对应编译器标志 SWIFT_APPROACHABLE_CONCURRENCY
  2. 搜索 "Default Actor Isolation",设为 MainActor

这两步会同时启用以下 Swift 特性标志:

  • NonisolatedNonsendingByDefault — nonisolated 异步函数默认继承调用者隔离
  • InferIsolatedConformances — 自动推断协议一致性的隔离
  • GlobalActorIsolatedTypesUsability — 改善全局 Actor 类型的使用体验

Swift Package 中启用

如果你在写 Swift Package,在 Package.swift 中配置 swiftSettings

// Package.swift
let package = Package(
    name: "MyLibrary",
    platforms: [.iOS(.v26)],
    targets: [
        .target(
            name: "MyLibrary",
            swiftSettings: [
                .defaultIsolation(MainActor.self),
                .enableUpcomingFeature("NonisolatedNonsendingByDefault"),
                .enableUpcomingFeature("InferIsolatedConformances"),
            ]
        ),
    ]
)

其中 .defaultIsolation(MainActor.self) 是 Swift 6.2 新增的 Package Manifest API,语义很直观。

已有项目迁移实战

启用这些特性之后,你的已有代码可能会有行为变化。下面这几个是最容易踩的坑。

1. nonisolated 异步函数不再跑到后台了

这是迁移过程中最容易出问题的地方,务必注意。以前依赖 nonisolated 异步函数自动在后台执行的代码,现在会留在调用者的 Actor 上:

class HeavyLifter {
    // 在 Swift 6.0 中,这个函数自动在后台线程运行
    // 在 Swift 6.2 中(启用 NonisolatedNonsendingByDefault),
    // 它会留在调用者的线程上!
    nonisolated func crunchNumbers() async -> [Double] {
        // 如果从 MainActor 调用,这段代码会阻塞主线程!
        return (0..<1_000_000).map { Double($0) * Double($0) }
    }
}

修复很简单——给需要在后台运行的函数加上 @concurrent

class HeavyLifter {
    @concurrent
    func crunchNumbers() async -> [Double] {
        // ✅ 现在明确在后台线程运行
        return (0..<1_000_000).map { Double($0) * Double($0) }
    }
}

2. 全局变量默认隔离到 MainActor

启用默认 MainActor 隔离后,全局变量和静态属性会自动归属 MainActor。如果你从后台线程访问它们,编译器会报错:

// 这个全局变量现在默认隔离到 MainActor
var globalCache: [String: Data] = [:]

@concurrent
func backgroundTask() async {
    // ⚠️ 错误:不能从非 MainActor 上下文访问 MainActor 隔离的属性
    globalCache["key"] = someData
}

// 修复方案一:标记为 nonisolated
nonisolated var globalCache: [String: Data] = [:]

// 修复方案二:用 Actor 封装(更推荐)
actor CacheManager {
    var cache: [String: Data] = [:]

    func set(_ key: String, data: Data) {
        cache[key] = data
    }
}

3. 迁移检查清单

建议按这个顺序来,别着急一步到位:

  1. 先在一个模块上试点——千万不要一次性全量启用
  2. 审查所有 nonisolated async 函数——确认哪些需要加 @concurrent
  3. 检查全局变量和静态属性——决定它们应该隔离到 MainActor 还是标记为 nonisolated
  4. 跑一遍完整测试——特别关注跟线程相关的行为变化
  5. 做性能测试——确保没有把本应在后台跑的重活塞到了主线程

实战示例:一个完整的 MVVM 应用

光讲概念总是抽象的。来看一个完整的例子,展示在 Swift 6.2 Approachable Concurrency 下怎么组织代码结构:

import SwiftUI

// ========================================
// 模型层 — 标记为 nonisolated,不绑定任何 Actor
// ========================================
nonisolated struct Article: Codable, Identifiable, Sendable {
    let id: UUID
    let title: String
    let body: String
    let publishedAt: Date
}

// ========================================
// 网络层 — 有些方法需要后台执行
// ========================================
class ArticleService {
    private let baseURL = URL(string: "https://api.example.com")!

    // 网络请求:I/O bound,用默认的 nonsending 就好
    // 从 MainActor 调用时会在 MainActor 上运行
    // 但 await URLSession 时会挂起,不会阻塞主线程
    nonisolated func fetchArticles() async throws -> [Article] {
        let url = baseURL.appendingPathComponent("articles")
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([Article].self, from: data)
    }

    // 大数据解析:CPU bound,应该在后台执行
    @concurrent
    func parseHugeDataset(_ data: Data) async throws -> [Article] {
        // 这可能需要几百毫秒,不应阻塞主线程
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        return try decoder.decode([Article].self, from: data)
    }
}

// ========================================
// ViewModel — 默认在 MainActor 上,完美适配 SwiftUI
// ========================================
@Observable
class ArticleListViewModel {
    // 这些属性默认就在 MainActor 上,SwiftUI 可以安全地绑定
    var articles: [Article] = []
    var isLoading = false
    var errorMessage: String?

    private let service = ArticleService()

    func loadArticles() async {
        isLoading = true
        errorMessage = nil

        do {
            // fetchArticles 是 nonisolated(nonsending),
            // 继承 MainActor 隔离——但 await 时会挂起,不阻塞 UI
            articles = try await service.fetchArticles()
        } catch {
            errorMessage = "加载失败:\(error.localizedDescription)"
        }

        isLoading = false
    }
}

// ========================================
// View — 正常写 SwiftUI,没有任何变化
// ========================================
struct ArticleListView: View {
    @State private var viewModel = ArticleListViewModel()

    var body: some View {
        NavigationStack {
            Group {
                if viewModel.isLoading {
                    ProgressView("加载中...")
                } else if let error = viewModel.errorMessage {
                    ContentUnavailableView(
                        "出错了",
                        systemImage: "exclamationmark.triangle",
                        description: Text(error)
                    )
                } else {
                    List(viewModel.articles) { article in
                        VStack(alignment: .leading, spacing: 4) {
                            Text(article.title)
                                .font(.headline)
                            Text(article.body)
                                .font(.subheadline)
                                .foregroundStyle(.secondary)
                                .lineLimit(2)
                        }
                    }
                }
            }
            .navigationTitle("文章列表")
            .task {
                await viewModel.loadArticles()
            }
        }
    }
}

注意看这段代码有多干净——没有一个 @MainActor 注解出现,但 ViewModel 的属性更新完全是线程安全的。唯一需要你做的并发决策,就是给 CPU 密集型的 parseHugeDataset 加了个 @concurrent

这才是并发编程该有的样子嘛。

常见编译器错误与解决方案

启用 Approachable Concurrency 后,你大概率会遇到一些新的编译器错误。别慌,基本上都有固定的解法。

错误 1:跨 Actor 隔离域访问属性

// 错误信息:Main actor-isolated property cannot be referenced
// from a @concurrent function
var cachedData: Data?

@concurrent
func processInBackground() async {
    let data = cachedData // ❌ 不能从后台函数访问 MainActor 属性
}

解决方案:先在 MainActor 上获取值,再传给后台函数:

func startProcessing() async {
    let data = cachedData // ✅ 在 MainActor 上安全访问
    await processInBackground(data: data)
}

@concurrent
func processInBackground(data: Data?) async {
    guard let data else { return }
    // 处理数据...
}

错误 2:nonisolated 异步函数中修改 Actor 状态

actor DatabaseManager {
    var records: [Record] = []

    // 这个函数是 nonisolated(nonsending)
    // 但它试图修改 actor 的内部状态
    nonisolated func importRecords(_ newRecords: [Record]) async {
        records.append(contentsOf: newRecords) // ❌ 不能从非隔离上下文修改
    }
}

解决方案:去掉 nonisolated,让方法保持在 Actor 隔离内:

actor DatabaseManager {
    var records: [Record] = []

    // ✅ 作为 actor 的隔离方法,可以安全修改内部状态
    func importRecords(_ newRecords: [Record]) {
        records.append(contentsOf: newRecords)
    }
}

错误 3:第三方库的 Sendable 问题

这个是迁移过程中比较头疼的——有些第三方库还没有跟进 Swift 6.2 的适配,它们的类型可能不是 Sendable 的。当你把这些类型传入 @concurrent 函数时就会报错:

// 第三方库的类型
// class ThirdPartyResult { ... } // 非 Sendable

@concurrent
func processResult(_ result: ThirdPartyResult) async {
    // ❌ ThirdPartyResult 不是 Sendable 的
}

临时解决方案:用 @preconcurrency import 先抑制警告,或者干脆改用默认的 nonisolated(nonsending)来绕过跨隔离域的限制:

@preconcurrency import ThirdPartyLib

// 或者:既然不真正需要后台执行,就用默认的 nonsending
nonisolated func processResult(_ result: ThirdPartyResult) async {
    // ✅ 不跨隔离域,不需要 Sendable
}

(等库作者适配之后记得回来改掉就好。)

性能注意事项

Approachable Concurrency 确实让代码写起来舒服多了,但有一个潜在陷阱你一定要注意:别无意间把 CPU 密集型工作丢到了主线程上

在 Swift 6.0 中,nonisolated 异步函数会自动跑后台,所以即使你不小心在里面做了重活,至少不会卡 UI。但 Swift 6.2 改了默认行为——这些函数现在继承调用者隔离,如果调用者在 MainActor 上,你的"重活"就直接在主线程跑了。

开发阶段可以用这个简单的检测方法:

nonisolated func suspiciouslySlowFunction() async {
    // 在调试时加上这行来检查当前线程
    #if DEBUG
    if Thread.isMainThread {
        print("⚠️ \(#function) 正在主线程运行,考虑加 @concurrent")
    }
    #endif

    // 你的代码...
}

更靠谱的做法是上 Instruments 的 Time Profiler,看主线程的占用情况。发现某个异步函数在主线程上耗时过长?给它加个 @concurrent 就搞定了。

常见问题(FAQ)

Swift 6.2 Approachable Concurrency 会破坏我的现有代码吗?

不会自动破坏——这点可以放心。对于已有项目,Approachable Concurrency 是可选启用的,你得手动在 Build Settings 中开启才行。新建项目则是默认启用。不过一旦启用,nonisolated 异步函数的运行位置会改变(从全局执行器变成调用者的 Actor),所以迁移前务必审查那些依赖后台执行的代码。

@concurrent 和 Task.detached 有什么区别?

@concurrent 标记在函数声明上,表示该函数始终在全局并发执行器上运行——这是函数的固有属性。而 Task.detached 是在调用侧创建一个脱离当前 Actor 上下文的新任务。前者更简洁、意图更明确;后者更灵活,但容易丢失结构化并发的好处。在 Swift 6.2 中,绝大多数场景都应该优先用 @concurrent

启用 MainActor 默认隔离后,SwiftUI 代码需要改动吗?

基本不需要。SwiftUI 的 View 协议从 iOS 18 起就已经隐式标记为 @MainActor 了,所以你的 View 层本来就在主线程上。启用默认隔离后,之前手动加的那些 @MainActor 注解变成了冗余——可以删掉让代码更干净,当然留着也不碍事。主要的改动集中在 ViewModel 和 Service 层:检查是否有依赖后台执行的 nonisolated async 方法,该加 @concurrent 的加上。

nonisolated(nonsending) 函数中的 await 会阻塞主线程吗?

不会,这点很重要。await 是一个挂起点(suspension point),执行到 await 时函数会挂起当前任务并释放线程。比如 await URLSession.shared.data(from: url) 在等网络响应的时候,主线程是完全空闲的,可以正常处理 UI 事件。真正会占用主线程的是 await 前后的同步代码——如果那部分有 CPU 密集型计算,那才需要考虑用 @concurrent

我应该先迁移到 Swift 6.0 还是直接跳到 Swift 6.2?

如果你的项目还在 Swift 5 上,我的建议是直接上 Swift 6.2。Swift 6.0 的严格并发检查对很多项目来说实在太激进了(我见过上千个 Sendable 错误的场景),而 Swift 6.2 的 Approachable Concurrency 让这个过渡平滑了很多。先启用 Swift 6 语言模式,然后立刻开启 Approachable Concurrency 来减少编译器错误——这是目前最省心的迁移路径。

关于作者 Editorial Team

Our team of expert writers and editors.