xDocxDoc
AI
前端
后端
iOS
Android
Flutter
AI
前端
后端
iOS
Android
Flutter
  • Swift Approachable Concurrency(易用并发)

Swift Approachable Concurrency(易用并发)

Swift 6.2 引入的 Approachable Concurrency(易用并发)是一组旨在简化并发编程的语言特性和编译设置,其核心理念是默认在主线程(Main Actor)上执行代码,仅在必要时才将任务移至后台线程。这对于移动应用开发尤其重要,因为大多数 iOS 和 iPadOS 应用主要在主线程上运行,只有少数耗时任务需要后台处理以保持 UI 响应性。在 Xcode 26 中创建的新项目会默认启用 Approachable Concurrency,而现有项目则需要手动修改构建设置。对于 Swift Packages,则需要满足特定的工具版本和配置要求。

1. Approachable Concurrency 的核心概念

Approachable Concurrency 的设计目标是让并发编程对开发者更加友好,减少难以理解和解决的编译器错误和警告,这些错误有时甚至与代码的实际问题无关。它通过一系列编译器特性来实现这一目标。

1.1 默认主线程隔离 (Default Actor Isolation to MainActor)

Approachable Concurrency 强制所有自定义类型默认运行在主线程(Main Actor)上,这为代码提供了天然的线程安全性。这意味着,除非开发者明确指定,否则代码默认就在主 Actor 上执行。这与另一个重要的并发特性“默认使用主 Actor”一同引入,强化了所有函数默认在主 Actor 上运行的原则。

1.2 Nonisolated(nonsending) by Default

另一个关键特性是 nonisolated(nonsending) by default。它确保了 nonisolated async 的异步函数默认在调用 Actor 的执行器上运行,而不是在全局执行器上。这种行为统一了非异步 nonisolated 函数的行为。例如,一个标记为 nonisolated 的方法会继承其调用者的线程上下文,如果由主 Actor 调用,它就在主线程运行;如果由后台 Actor 调用,它则在后台运行。

1.3 推断隔离遵从性 (Infer Isolated Conformances)

此特性引入了“隔离遵从性”(isolated conformance)的概念,它将协议遵从性限制在与遵从类型相同的隔离域内。例如,如果一个类型被隔离到主 Actor,那么其协议遵从性(如 Equatable)也会被自动推断为隔离到主 Actor,从而避免了潜在的运行时错误。

2. 在 Swift Packages 中启用 Approachable Concurrency

在 Swift Packages 中启用 Approachable Concurrency 需要满足特定的配置要求。

2.1 工具版本要求

首先,Package 的 swift-tools-version 需要设置为 6.2 或更高版本。这是在 Package.swift 文件中指定的。

// swift-tools-version:6.2
import PackageDescription

let package = Package(
    name: "YourPackage",
    platforms: [.iOS(.v16), .macOS(.v13)],
    products: [ ... ],
    dependencies: [ ... ],
    targets: [
        .target(
            name: "YourTarget",
            swiftSettings: [
                // Swift 设置将在这里配置
            ]
        )
    ]
)

2.2 配置 Swift Settings

在目标的 swiftSettings 中,你需要启用 Approachable Concurrency 相关的特性标志。这通常包括设置默认隔离到 MainActor 以及启用一些“即将到来的特性”(Upcoming Features)。

.target(
    name: "YourTarget",
    swiftSettings: [
        .enableUpcomingFeature("ApproachableConcurrency"), // 启用 Approachable Concurrency
        .defaultIsolation(.mainActor), // 设置默认隔离域为 MainActor
        .enableUpcomingFeature("InferIsolatedConformances"), // 启用推断隔离遵从性
        .enableUpcomingFeature("NonisolatedNonsendingByDefault") // 启用 Nonisolated nonsending 默认行为
    ]
)
  • .enableUpcomingFeature("ApproachableConcurrency"): 这是启用 Approachable Concurrency 套件的主开关。
  • .defaultIsolation(.mainActor): 将包中类型的默认隔离域设置为主 Actor。
  • .enableUpcomingFeature("InferIsolatedConformances"): 启用推断隔离遵从性,自动将协议遵从性隔离到与类型相同的 Actor。
  • .enableUpcomingFeature("NonisolatedNonsendingByDefault"): 启用 nonisolated 方法默认继承调用者执行器的行为。

2.3 不同 Swift 版本下的行为差异

需要注意的是,这些设置的效果会因 Package 所采用的 Swift 语言版本而异:

  • Swift 6 模式:许多 Approachable Concurrency 的特性(如 Global-Actor-Isolated Types Usability, Disable Outward Actor Isolation Inference)已经是默认开启的。启用 Approachable Concurrency 主要是为了在 Swift 6 中获得更一致的体验和修复一些边界情况。
  • Swift 5 模式:在 Swift 5 语言模式下,这些特性默认都是关闭的。因此,在现有项目或包中,显式启用这些设置对于获得 Approachable Concurrency 的好处至关重要。

3. Approachable Concurrency 的底层编译器设置

当你在 Xcode 项目或 Swift Package 中启用 Approachable Concurrency 时,底层是通过一系列编译器标志来实现的。

3.1 核心编译器标志

Approachable Concurrency 主要涉及以下编译器设置:

  • Infer Isolated Conformances: 自动将协议遵从性隔离到与类型相同的全局 Actor。
  • nonisolated(nonsending) By Default: 改变 nonisolated async 函数的行为,使其默认在调用者的执行器上运行。
  • Global-Actor-Isolated Types Usability: 让编译器更智能地处理全局 Actor 隔离类型的可用性,减少对 nonisolated(unsafe) 的需求。
  • Disable Outward Actor Isolation Inference: 修复了早期 Swift 并发中一个令人困惑的行为,即使用具有 Actor 隔离的属性包装器(如 @StateObject)会导致整个类型被隐式隔离到该 Actor。

3.2 在 Xcode 项目中启用

对于 Xcode 项目,你可以在项目的构建设置中启用它:

  1. 打开 Xcode 项目。
  2. 选择你的 Target。
  3. 进入 "Build Settings"。
  4. 搜索 "Approachable Concurrency" 或 "approachable"。
  5. 将 "Approachable Concurrency" 设置改为 "Yes"。

4. 实践案例与代码分析

理解 Approachable Concurrency 如何影响代码的实际执行线程至关重要。

4.1 基础用法示例

假设我们有一个默认启用 Approachable Concurrency 的 Swift Package。其中的类型和方法会表现出以下行为:

// 该包已启用 .defaultIsolation(.mainActor)
// 因此 MyModel 默认被隔离到 MainActor
class MyModel {
    func updateUI() async { ... } // 此方法在主线程运行

    nonisolated
    func fetchData() async { ... } // 此方法继承调用者的线程

    @concurrent
    func heavyProcessing() async { ... } // 此方法强制在后台运行
}
  • updateUI(): 由于类默认隔离到 MainActor,此方法会在主线程执行。
  • fetchData(): 标记为 nonisolated,它会继承调用者的执行器。如果从主 Actor 调用,它在主线程运行;如果从后台任务调用,它在后台运行。
  • heavyProcessing(): 使用 @concurrent 宏明确标记,它会始终在后台线程执行,适用于计算密集型任务。

4.2 线程流向分析

考虑一个更复杂的例子,分析每个方法的执行线程:

class ConcurrentDemo {
    func firstMethod() async {           // ①
        await secondMethod()
        await thirdMethod()
    }
    
    @concurrent
    func secondMethod() async {          // ②
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        await thirdMethod()
    }
    
    nonisolated
    func thirdMethod() async { ... }     // ③
}

// 在某个 View 中调用
struct ContentView: View {
    var body: some View {
        VStack { Text("Hello") }
            .task {
                Task.detached { // 此 Task.detached 在后台运行
                    let demo = ConcurrentDemo()
                    await demo.firstMethod() // 调用起点
                }
            }
    }
}

其执行顺序与线程分析如下:

步骤方法执行线程原因分析
①firstMethod()主线程ConcurrentDemo 默认隔离到 MainActor。尽管从 Task.detached(后台)调用,但 Actor 隔离决定了它必须回到主线程。
②secondMethod()后台线程标记了 @concurrent,强制该方法在后台执行。
③-1thirdMethod()后台线程在 secondMethod()(后台)内调用,nonisolated 方法继承调用者(后台)的线程上下文。
③-2thirdMethod()主线程在 firstMethod()(主线程)内调用,nonisolated 方法继承调用者(主线程)的线程上下文。

这个例子清晰地展示了 await 关键字负责挂起和恢复执行,而 @concurrent 和 nonisolated 才是决定方法最终在哪个线程上运行的关键因素。

4.3 与协议和闭包的交互

4.3.1 隔离遵从性实践

推断隔离遵从性特性在处理协议时非常有用。考虑以下代码:

@MainActor
class MyModel: Decodable {
    // 实现 Decodable 的要求...
}

在启用 InferIsolatedConformances 之前,虽然 MyModel 本身被隔离到 @MainActor,但其对 Decodable 协议的遵从性可能不在主 Actor 上,这可能导致编译器错误或潜在的运行时问题。启用该特性后,编译器会自动将 Decodable 的遵从性推断为同样隔离在 @MainActor 上,确保了安全性。

4.3.2 闭包捕获与 @Sendable

在并发代码中传递闭包时,必须确保其捕获的值是线程安全的(即 Sendable)。Approachable Concurrency 并没有改变这一基本规则。

class NonSendableManager {
    var data: String = ""
}

class MyViewModel {
    let manager = NonSendableManager() // 非 Sendable 类型

    func fetch() {
        Task { // 这个闭包是 @Sendable 的
            manager.data = "new data" // ❌ 编译错误:捕获了非 Sendable 的 'self'
        }
    }
}

解决方案:

  • 使用局部变量:将需要的数据提取到局部变量中。
    func fetch() {
        let newData = "new data"
        Task {
            // 使用 newData,而不是捕获 self.manager
        }
    }
  • 将类型标记为 @MainActor:如果整个类型都涉及 UI 操作,将其隔离到 MainActor 上,编译器会将其视为 Sendable。
    @MainActor
    class MyViewModel {
        // ... 现在在 MainActor 上,共享状态是安全的
        func fetch() {
            Task { @MainActor in // 确保闭包也在 MainActor 上执行
                manager.data = "new data" // 现在安全了
            }
        }
    }
  • 谨慎使用 @unchecked Sendable:如果你能确保线程安全但编译器无法验证,可以使用 @unchecked Sendable,但这将责任转移给了开发者。

5. 已知问题与解决方案

Approachable Concurrency 目前仍处于测试阶段,开发者可能会遇到一些边界情况。

5.1 协议期望非隔离行为

一些系统协议(如 CodingKey)在设计上期望非隔离(nonisolated)行为。当你的类型默认被隔离到 MainActor 时,与这些协议的交互可能会产生编译器错误,因为协议要求的方法可能需要在非主线程上下文中执行。

解决方案:

  • 使用 @preconcurrency 属性:这个属性用于标记那些在 Swift 并发模型之前编写的代码或协议,告诉编译器放宽严格的并发检查,以保持向后兼容性。
    @preconcurrency
    extension YourModel: CodingKey { ... }
  • 选择性禁用隔离:对于特定需要与非隔离协议交互的方法,可以使用 nonisolated 关键字将其从 Actor 隔离中移除。
    @MainActor
    class YourModel {
        nonisolated func methodRequiredByCodingKey() { ... }
    }

5.2 性能

虽然 Approachable Concurrency 简化了并发编程,但仍需遵循性能最佳实践。

  • 明智地使用 @concurrent:不要过度使用 @concurrent。在结构化并发(如 TaskGroup)中,子任务默认已经在后台运行,无需额外标记。只有在方法内部包含大量 CPU 计算时,才需要添加 @concurrent 以确保其始终在后台执行,从而解放主线程。
    // 在 TaskGroup 中,addTask 内的代码默认在后台
    await withTaskGroup(of: Void.self) { group in
        for item in items {
            group.addTask {
                await self.process(item) // 如果 process 很耗时,再标记它为 @concurrent
            }
        }
    }
    
    @concurrent // 仅在必要时添加
    func process(_ item: Item) async { ... }
  • 使用 Instruments 进行分析:始终使用 Instruments 的并发模板来分析你的应用,识别性能瓶颈和线程使用情况,确保主线程保持流畅。

6. 总结

Swift 6.2 的 Approachable Concurrency 是 Swift 并发演进道路上重要的一步。它通过提供合理的默认值(主线程隔离)和更智能的编译器行为,显著降低了并发编程的门槛,特别是对于大多数以 UI 为中心、并发模式相对简单的移动应用开发者而言。

6.1 核心价值

  • 降低门槛:使单线程应用更易于编写,并为在需要时引入并发提供了更直接的路径。
  • 减少误解:通过更可预测的编译器错误和警告,消除了许多令人困惑的、甚至是为不存在的问题而存在的编译错误。
  • 平滑过渡:为现有项目从 Swift 5 向 Swift 6 严格的并发检查过渡提供了更好的体验。

6.2 在 Swift Packages 中的实践要点

在 Swift Packages 中成功采用 Approachable Concurrency 需要:

  1. 设置 swift-tools-version: 6.2。
  2. 在目标的 swiftSettings 中配置 .enableUpcomingFeature("ApproachableConcurrency")、.defaultIsolation(.mainActor) 等相关标志。
  3. 理解线程继承:掌握 nonisolated 方法继承调用者线程的新行为。
  4. 善用 @concurrent:仅对真正耗时的 CPU 密集型任务使用 @concurrent 宏。
  5. 处理边界情况:对期望非隔离行为的系统协议,使用 @preconcurrency 或 nonisolated 方法进行交互。
最后更新: 2025/9/10 15:21