mcpField
S
swift-concurrency

swift-concurrency

github.com/dpearson2699开发1.3k4.3k 次安装13 周前更新
代码仓库

关于 swift-concurrency

面向 Swift 开发者的并发编程技能,帮助审查、修复和编写适用于 Swift 6.3 及更高版本的并发代码。重点涵盖隔离与 Sendable 安全,并指导以尽量少的行为改动解决问题。

主要功能

  1. 01并发代码审查与修复
  2. 02Actor 隔离规则
  3. 03Sendable 安全指导
  4. 04结构化并发与取消
  5. 05AsyncSequence 与 AsyncStream
  6. 06Swift 6.2 并发变更说明

适用场景

  1. 01诊断并发编译错误
  2. 02修复数据竞争安全问题
  3. 03审查 Swift 并发设置
  4. 04选择任务与取消模式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swift-concurrency

安装后在 Claude Code 中输入/swift-concurrency确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
references/
approachable-concurrency.md
async-algorithms.md
bridging-interop.md
concurrency-patterns.md
diagnostics.md
swiftui-concurrency.md
synchronization-primitives.md

详细介绍

Swift 并发

审查、修复并编写面向 Swift 6.3+ 的并发 Swift 代码。将 Swift 6.4 / Xcode 27 beta 清理 API 放在明确的工具链和可用性检查之后。应用 Actor 隔离、Sendable 安全性和现代并发模式,同时尽量减少行为变更。

目录

排查工作流

诊断并发问题时,请按以下顺序操作:

步骤 1:收集上下文
  • 复制准确的编译器诊断信息和相关符号。
  • 确定项目的并发设置:
    • Swift 语言版本(必须为 6.2+)。
    • 用于版本特定功能和发行说明中所述变通方案的 Xcode/工具链版本。
    • 是否启用了 Approachable Concurrency。
    • Default Actor Isolation 是否设置为 MainActor。
    • Swift 6 严格并发状态:在 Swift 6 语言模式下为 complete/errors;仅在审查 Swift 5 迁移设置时使用 Complete / Targeted / Minimal。
  • 确定代码当前所在的 Actor 上下文(@MainActor、自定义 actor、nonisolated),以及是否启用了默认隔离模式。
  • 确认代码是否与 UI 绑定,或是否打算在主 Actor 之外运行。
步骤 2:应用最小的安全修复

优先选择既能满足数据竞争安全要求,又能保留现有行为的修改。

情况建议的修复
与 UI 绑定的类型为类型或相关成员添加 @MainActor。
MainActor 类型上的协议一致性使用隔离的一致性:extension Foo: @MainActor Proto。
全局 / 静态状态使用 @MainActor 保护,或将其移入 Actor。
需要后台工作在 nonisolated 类型上使用 @concurrent 异步函数。
Sendable 错误优先使用不可变值类型。仅在确实适用时添加 Sendable。
跨隔离回调使用 sending 参数(SE-0430)进行更精细的控制。
步骤 3:验证
  • 重新构建并确认诊断信息已解决。
  • 检查是否引入了新的警告。
  • 确保没有添加不必要的 @unchecked Sendable 或 nonisolated(unsafe)。
  • 审查构建设置时,止步于设置和最小的代码级修复。除非提示要求诊断或迁移,否则不要添加 Thread Sanitizer、宽泛的迁移顺序建议或架构建议。

Swift 6.2 语言变更

Swift 6.2 引入了“approachable concurrency”(易用并发)——一组语言变更,默认让并发代码更加安全,同时减少注解负担。在 Xcode 中,Approachable Concurrency 和 Default Actor Isolation 是独立的构建设置:使用 Approachable Concurrency 启用捆绑的 upcoming-feature 标志;如果希望未注解的代码被推断为 @MainActor,则将 Default Actor Isolation 设置为 MainActor。

SE-0466:默认 MainActor 隔离

使用 -default-isolation MainActor 编译器标志、SwiftPM .defaultIsolation(MainActor.self),或在 Xcode 中将 Default Actor Isolation 设置为 MainActor 时,模块中的未注解声明都会被推断为 @MainActor,除非显式选择退出。

效果: 对与 UI 绑定的代码以及全局 / 静态状态,无需到处编写 @MainActor,即可消除大多数数据竞争安全错误。

// With default MainActor isolation enabled, these are implicitly @MainActor:
final class StickerLibrary {
    static let shared = StickerLibrary()  // safe -- on MainActor
    var stickers: [Sticker] = []
}

final class StickerModel {
    let photoProcessor = PhotoProcessor()
    var selection: [PhotosPickerItem] = []
}

// Conformances are also implicitly isolated:
extension StickerModel: Exportable {
    func export() {
        photoProcessor.exportAsPNG()
    }
}

适用场景: 推荐用于应用、脚本和其他大多数代码与 UI 绑定的可执行目标。不推荐用于应保持 Actor 无关的库目标。

SE-0461:nonisolated(nonsending)

非隔离异步函数现在默认保留在调用方的 Actor 上,而不是跳转到全局并发执行器。这就是 nonisolated(nonsending) 行为。

class PhotoProcessor {
    func extractSticker(data: Data, with id: String?) async -> Sticker? {
        // In Swift 6.2+, this runs on the caller's actor (e.g., MainActor)
        // instead of hopping to a background thread.
        // ...
    }
}

@MainActor
final class StickerModel {
    let photoProcessor = PhotoProcessor()

    func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
        guard let data = try await item.loadTransferable(type: Data.self) else {
            return nil
        }
        // No data race -- photoProcessor stays on MainActor
        return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
    }
}

需要在后台执行时,使用 @concurrent 明确指定。

@concurrent 属性

@concurrent 可确保函数始终在并发线程池中运行,让调用方的 Actor 可以执行其他任务。

class PhotoProcessor {
    var cachedStickers: [String: Sticker] = [:]

    func extractSticker(data: Data, with id: String) async -> Sticker {
        if let sticker = cachedStickers[id] { return sticker }

        let sticker = await Self.extractSubject(from: data)
        cachedStickers[id] = sticker
        return sticker
    }

    @concurrent
    static func extractSubject(from data: Data) async -> Sticker {
        // Expensive image processing -- runs on background thread pool
        // ...
    }
}

要将函数移到后台线程,请同时使用以下两种退出隔离的方式:

  1. 确保所在类型为 nonisolated,或者该函数可以从非隔离上下文调用。
  2. 为卸载的函数添加 @concurrent。仅使用 nonisolated 不会将 CPU 密集型工作移出调用方的 Actor。
  3. 如果函数尚未异步执行,则添加 async。
  4. 在调用处添加 await。
nonisolated struct PhotoProcessor {
    @concurrent
    func process(data: Data) async -> ProcessedPhoto? { /* ... */ }
}

// Caller:
processedPhotos[item.id] = await PhotoProcessor().process(data: data)
SE-0472:Task.immediate

Task.immediate 会在任何挂起点之前,于当前 Actor 上同步开始执行,而不是将任务加入队列。

Task.immediate { await handleUserInput() }

适用于需要立即开始的延迟敏感型工作。此外,Task.immediateDetached 将立即开始与分离语义相结合。

SE-0475:事务性观察(Observations)

Observations { } 通过 AsyncSequence 异步观察 @Observable 类型,从而支持事务性变更跟踪。

for await _ in Observations { model.count } {
    print("Count changed to \(model.count)")
}
隔离的符合性

需要使用 MainActor 状态的符合性称为隔离的符合性。编译器会确保它只在匹配的隔离上下文中使用。

protocol Exportable {
    func export()
}

// Isolated conformance: only usable on MainActor
extension StickerModel: @MainActor Exportable {
    func export() {
        photoProcessor.exportAsPNG()
    }
}

@MainActor
struct ImageExporter {
    var items: [any Exportable]

    mutating func add(_ item: StickerModel) {
        items.append(item)  // OK -- ImageExporter is on MainActor
    }
}

如果 ImageExporter 是 nonisolated,添加 StickerModel 就会失败,并报错:“Main actor-isolated conformance of 'StickerModel' to 'Exportable' cannot be used in nonisolated context.”

时钟纪元

ContinuousClock 和 SuspendingClock 现在公开了 .epoch(SE-0473),支持在不同类型的时钟之间即时比较和转换。

let continuous = ContinuousClock()
let elapsed = continuous.now - continuous.epoch  // Duration since system boot

Actor 隔离规则

  1. 所有可变共享状态都必须由 Actor 或全局 Actor 保护。
  2. 所有接触 UI 的代码都使用 @MainActor,不例外。 全局 Actor 属于 Actor 隔离;对于绑定到 UI 的共享状态,请将 @MainActor 作为标准模式。
  3. 仅对访问不可变(let)属性或执行纯计算的方法使用 nonisolated。
  4. 使用 @concurrent 明确将工作移出调用方的 Actor。
  5. 除非已证明不存在数据竞争风险,且已穷尽更安全的替代方案,否则绝不使用 nonisolated(unsafe)。它是一个不安全的审查边界,而不是同步原语。对于同步并行循环中的狭义不安全指针捕获模式,请遵循桥接与互操作中的证明要求。每次迭代访问互不重叠,可以消除冲突的并发访问需求以及同步需求;访问互不重叠本身并不等于同步。
  6. 绝不在 Actor 内添加手动锁(NSLock、DispatchSemaphore)。

Sendable 规则

  1. 当所有存储属性均为 Sendable 时,值类型(结构体、枚举)会自动符合 Sendable。 对于可变引用类型的诊断,请先提取不可变的 Sendable 值快照或 DTO,而不要共享该引用。
  2. Actor 隐式符合 Sendable。
  3. @MainActor 类隐式符合 Sendable。不要添加冗余的 Sendable 符合性。
  4. 非 Actor 类必须为 final,且所有存储属性都必须是 let 和 Sendable。
  5. @unchecked Sendable 是最后手段。请说明编译器为何无法证明其安全性。
  6. 使用 sending 参数(SE-0430)进行更精细的隔离控制。
  7. 仅对无法修改的第三方库使用 @preconcurrency import。并计划移除它。

结构化并发模式

Async Defer(Swift 6.4+)

在 Swift 6.4+ 中,异步上下文里的 defer 块可以包含 await(SE-0493)。 用于异步清理:关闭连接、刷新缓冲区,或释放需要异步调用的 资源。

defer 主体会继承外围隔离,并在作用域退出时隐式等待。它不会抑制取消;清理代码中检查 Task.isCancelled 或 Task.checkCancellation() 时,仍会观察到取消。

func fetchData() async throws -> Data {
    let connection = try await openConnection()
    defer { await connection.close() }
    return try await connection.read()
}

Task: 非结构化,会继承调用方上下文。

Task { await doWork() }

Task.detached: 不继承上下文。仅在明确需要中断隔离继承时使用。

Task.immediate: 在当前 actor 上立即启动。用于对延迟敏感的工作。

Task.immediate { await handleUserInput() }

async let: 固定数量的并发操作。

async let a = fetchA()
async let b = fetchB()
let result = try await (a, b)

TaskGroup: 动态数量的并发操作。

try await withThrowingTaskGroup(of: Item.self) { group in
    for id in ids {
        group.addTask { try await fetch(id) }
    }
    for try await item in group { process(item) }
}

任务取消

  • 取消采用协作式机制。在循环中检查 Task.isCancelled 或调用 try Task.checkCancellation()。
  • 在 SwiftUI 中使用 .task 修饰符——视图消失时,它会处理取消。
  • 使用 withTaskCancellationHandler 执行清理。
  • Swift 6.4 / iOS 27+ beta:仅在必须于取消后完成的短暂清理或回滚中使用 withTaskCancellationShield。在 shield 内部,Task.isCancelled 为 false,且 Task.checkCancellation() 不会抛出;退出作用域后,取消会再次变得可观察。
  • 在 deinit 或 onDisappear 中取消已存储的任务。

Actor 重入

Actor 具有可重入性。状态可能在挂起点之间发生变化。

// WRONG: State may change during await
actor Counter {
    var count = 0
    func increment() async {
        let current = count
        await someWork()
        count = current + 1  // BUG: count may have changed
    }
}

// CORRECT: Mutate synchronously, no reentrancy risk
actor Counter {
    var count = 0
    func increment() { count += 1 }
}

AsyncSequence 和 AsyncStream

使用 AsyncStream 桥接回调/代理 API:

let stream = AsyncStream<Location> { continuation in
    let delegate = LocationDelegate { location in
        continuation.yield(location)
    }
    continuation.onTermination = { _ in delegate.stop() }
    delegate.start()
}

对于返回单个值的回调,使用 withCheckedContinuation / withCheckedThrowingContinuation。确保只恢复一次。

@Observable 与并发

  • 用于视图模型的 @Observable 类应标记为 @MainActor。
  • 使用 @State 持有 @Observable 实例(替代 @StateObject)。
  • 使用 Observations { }(SE-0475)将对 @Observable 属性的异步观察作为 AsyncSequence 执行。

同步原语

当 Actor 不适用时,例如需要同步访问、处理性能关键路径或桥接 C/ObjC,可以使用底层同步原语:

  • Actor 仍是异步共享状态的默认选择,适用于调用方可以挂起的场景;它们提供由编译器强制执行的隔离机制,并集成结构化并发,但外部调用会异步跨越 Actor,必须谨慎处理 await 处的可重入性,也不适用于同步 C 回调。对于与 UI 绑定的共享状态,请使用 @MainActor 等全局 Actor;切勿用 nonisolated(unsafe) 替代同步机制。
  • Mutex<Value>(iOS 18+,Synchronization 模块):新代码的首选锁。受保护的状态存储在锁内部。使用 withLock { } 模式。
  • OSAllocatedUnfairLock(iOS 16+,os 模块):面向较旧 iOS 版本时使用。支持所有权断言,便于调试。
  • Atomic<Value>(iOS 18+,Synchronization 模块):用于独立计数器和标志的无锁原子操作。Atomic 符合 Sendable,可以存储在 Sendable 容器类型中。.relaxed 仅用于独立指标;协调其他数据时,请使用获取/释放内存顺序或锁。

关键规则: 切勿在 Actor 中使用锁(重复同步),也切勿在 await 期间持有锁(这会在线程挂起时阻塞线程,可能导致协作线程池资源耗尽或死锁)。有关完整的 API 详情、代码示例以及锁与 Actor 的选择指南,请参阅 references/synchronization-primitives.md。 Mutex.withLock 和 OSAllocatedUnfairLock.withLock 使用同步闭包; 这种 API 形式确保临界区不会挂起。 使用运行时 if #available(iOS 18, *) 对 Mutex 和 Atomic 进行版本判断,切勿使用 #if swift(...) 或平台编译时检查。对于 NSLock,只需纠正其不符合 Sendable 的错误前提,避免解释符合性机制。 如果旧版锁包装器确实需要 @unchecked Sendable,请明确说明不变量: 所有可变状态均为私有,所有访问都使用同一把锁,不会泄露可变引用,并且不会在 await 期间持有锁。

常见错误

  1. 阻塞主执行器。 在 @MainActor 上执行繁重计算会导致 UI 卡顿。 将其移到 @concurrent 函数中。
  2. 不必要的 @MainActor。 网络层、数据处理和模型代码 不需要 @MainActor。只有涉及 UI 的代码才需要。
  3. 为无状态代码使用 Actor。 没有可变状态就不需要 Actor。使用普通的 结构体或函数。
  4. 为不可变数据使用 Actor。 使用 Sendable 结构体,而不是 Actor。
  5. 没有充分理由就使用 Task.detached。 这会丢失优先级、任务局部值 和取消传播。
  6. 忘记取消任务。 存储 Task 引用并取消它们,或使用 .task 视图修饰符。
  7. Task 中的循环引用。 在长期存储的任务中捕获 self 时,使用 [weak self]。
  8. 在异步上下文中使用信号量。 DispatchSemaphore.wait() 可能阻塞协作式执行器线程;如果发信号的一方需要被阻塞的执行器,还可能导致死锁。使用异步原语或结构化并发。
  9. 隔离方式不一致。 在同一类型中混用 @MainActor 和 nonisolated 属性。应对整个类型采用一致的隔离方式。
  10. 用 MainActor.run 代替静态隔离。 优先使用 @MainActor func, 而不是 await MainActor.run { }。
  11. 默认使用 GCD 编排新的异步操作。 优先使用 async/await、 Actor 和任务组。在 API 要求使用队列、使用自定义执行器或进行有界的旧代码互操作时,保留派发队列。经过测量的、 同步、受 CPU 限制的并行 for 循环仍可使用 DispatchQueue.concurrentPerform;任务组是异步设计 的替代方案,并不能直接替换同步 API。遵循 桥接与互操作中的捕获与内存安全审查要求。 每次审查此诊断时,都要明确说明其 @Sendable 操作与两个非 Sendable 不安全缓冲区视图的关联,并将任何 豁免限制在已验证的本地基指针绑定上。每次保留并行路径的审查都必须要求进行基准测试与串行执行的对比、串行结果检查,以及 嵌套并行性检查。

审查清单

  • 所有可变共享状态都由 Actor 隔离
  • 没有数据竞争(没有未经保护的跨隔离访问)
  • 不再需要任务时会取消它们
  • @MainActor 上没有阻塞调用
  • Actor 内没有手动锁
  • Sendable 一致性正确(没有不合理的 @unchecked)
  • 已处理 Actor 重入(跨越 await 时不假设状态不变)
  • 已为 @preconcurrency 导入记录说明和移除计划
  • 繁重工作使用 @concurrent,而不是 @MainActor
  • 在 SwiftUI 中使用 .task 修饰符,而不是手动管理 Task
  • 同步并行循环已证明内存访问互不冲突,且指针 生命周期不变量成立

参考资料

评论

支持 Markdown