mcpField
A
axiom-swift-concurrency

axiom-swift-concurrency

github.com/charleswiltgen开发1.2k202 次安装今天更新
代码仓库

关于 axiom-swift-concurrency

面向 Swift 开发者的并发指南,按需讲解从异步到并发与 Actor 的演进,并帮助诊断 Swift 6 并发安全问题。重点是以隔离域而非线程来规划工作。

主要功能

  1. 01循序介绍并发演进
  2. 02讲解 async/await 与任务
  3. 03说明 @MainActor、Actor 与隔离
  4. 04指导 Sendable 与数据竞争排查
  5. 05提供后台 CPU 工作模式

适用场景

  1. 01规划新 Swift 项目的并发策略
  2. 02排查 Actor 隔离或 Sendable 错误
  3. 03将委托回调改为异步模式
  4. 04将耗时 CPU 工作移出主线程

安装

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

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-swift-concurrency

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

详细介绍

Swift 6 并发指南

目的:循序渐进地从单线程代码走向并发 Swift 代码 Swift 版本:Swift 6.3(默认启用严格并发)。@concurrent 需要 Swift 6.2+。 iOS 版本:iOS 17+(@concurrent 需要 iOS 26+) Xcode:Xcode 16+(@concurrent 需要 Xcode 26+)

何时使用此技能

✅ 在以下情况下使用此技能:

  • 开始新项目并决定并发策略
  • 调试 Swift 6 并发错误(Actor 隔离、数据竞争、Sendable 警告)
  • 决定何时引入 async/await 或并发
  • 实现 @MainActor 类或异步函数
  • 将代理回调转换为异步安全模式
  • 在 @MainActor、nonisolated、@concurrent 或 Actor 隔离之间做出选择
  • 解决 “Sending 'self' risks causing data races” 错误
  • 让类型遵循 Sendable
  • 将 CPU 密集型工作转移到后台线程
  • UI 响应迟钝,且性能分析显示主线程是瓶颈

❌ 不要将此技能用于:

  • 通用 Swift 语法(请参阅 Swift 文档)
  • SwiftUI 专属模式(请使用 axiom-swiftui-debugging 或 axiom-swiftui-performance)
  • API 专属模式(请参阅 API 文档)

核心理念:从隔离域出发思考,而不是线程

“你的应用应该从所有代码都在主线程上运行开始,而单线程代码能让你走得很远。”——Apple

不要再问:“这应该在哪个线程上运行?” 开始这样问:“哪个隔离域应该拥有这项工作?”

  • @MainActor → UI 状态所有权
  • 自定义 actor → 共享可变状态所有权
  • nonisolated → 无所有权,由调用方决定
  • @concurrent → 强制在后台执行

异步不代表后台执行。 async 函数会挂起而不会阻塞,但恢复执行时仍在调用它的 同一个 Actor 上。@MainActor 异步函数全程都在主 Actor 上运行——await 只会让出控制权,不会切换线程。当你需要强制在调用方 Actor 之外执行工作时,请使用 @concurrent(Swift 6.2+)。

优先使用结构化并发。 使用 async let 和 TaskGroup 执行并行工作——它们会自动在任务树中传播取消和错误。非结构化的 Task {} 用于衔接同步→异步边界(事件处理器、SwiftUI .task)。Task.detached 是最后的选择。

GCD 是一种桥接模式,而不是默认选择。 在新代码中,不要将 DispatchQueue、DispatchGroup、DispatchSemaphore 或完成处理器作为主要架构。仅在桥接尚无异步替代方案的旧版 API 时使用它们。隔离桥接代码,并让代码库的其余部分保持符合 Swift 6 惯例的写法。

渐进式演进之路
Single-Threaded → Asynchronous → Concurrent → Actors
     ↓                ↓             ↓           ↓
   Start here    Hide latency   Background   Move data
                 (network)      CPU work     off main

何时进阶:

  1. 保持单线程,如果 UI 响应流畅且操作很快
  2. 添加 async/await,当高延迟操作(网络、文件 I/O)会阻塞 UI 时
  3. 添加并发,当 CPU 密集型工作(图像处理、解析)会导致 UI 卡顿时
  4. 添加 Actor,当过多主 Actor 代码造成争用时

关键要点:并发代码更复杂。只有在性能分析表明确有需要时,才引入并发。


步骤 1:单线程代码(从这里开始)

启用主 Actor 模式(Xcode 26+ 新项目的默认设置)后,除非明确标记为其他方式,否则所有代码都在主线程上运行。

// ✅ Simple, single-threaded
class ImageModel {
    var imageCache: [URL: Image] = [:]

    func fetchAndDisplayImage(url: URL) throws {
        let data = try Data(contentsOf: url)  // Reads local file
        let image = decodeImage(data)
        view.displayImage(image)
    }

    func decodeImage(_ data: Data) -> Image {
        // Decode image data
        return Image()
    }
}
主 Actor 模式(Xcode 26+)
  • 新项目默认启用
  • 除非明确标记为其他方式,否则所有代码都受 @MainActor 保护
  • 无需担心并发访问,即可安全访问共享状态
构建设置(Xcode 26+)
Build Settings → Swift Compiler — Language
→ "Default Actor Isolation" = Main Actor

Build Settings → Swift Compiler — Upcoming Features
→ "Approachable Concurrency" = Yes

适用情况:如果所有操作都很快(60 fps 下低于 16 ms),就保持单线程!


步骤 2:异步任务(隐藏延迟)

当等待数据(网络、文件 I/O)会导致 UI 卡顿时,添加 async/await。

问题:网络访问会阻塞 UI
// ❌ Blocks main thread until network completes
func fetchAndDisplayImage(url: URL) throws {
    let data = try Data(contentsOf: url)  // ❌ Synchronous network fetch, freezes UI!
    let image = decodeImage(data)
    view.displayImage(image)
}
解决方案:Async/Await
// ✅ Suspends without blocking main thread
func fetchAndDisplayImage(url: URL) async throws {
    let (data, _) = try await URLSession.shared.data(from: url)  // ✅ Suspends here
    let image = decodeImage(data)  // ✅ Resumes here when data arrives
    view.displayImage(image)
}

会发生什么:

  1. 函数在主线程上启动
  2. await 会挂起函数,但不会阻塞主线程
  3. URLSession 在后台线程上获取数据(由库处理)
  4. 数据到达后,函数会在主线程上恢复执行
  5. 整个过程中,UI 始终保持响应
创建任务

响应用户事件创建任务:

class ImageModel {
    var url: URL = URL(string: "https://swift.org")!

    func onTapEvent() {
        Task {  // ✅ Create task for user action
            do {
                try await fetchAndDisplayImage(url: url)
            } catch {
                displayError(error)
            }
        }
    }
}
任务交错执行(重要概念)

多个异步任务可以通过轮流执行,在同一线程上运行:

Task 1: [Fetch Image] → (suspend) → [Decode] → [Display]
Task 2: [Fetch News]  → (suspend) → [Display News]

Main Thread Timeline:
[Fetch Image] → [Fetch News] → [Decode Image] → [Display Image] → [Display News]

优点:

  • 主线程不会闲置
  • 任务会尽快取得进展
  • 尚未涉及并发——仍然是单线程!

关键点:上面的两个任务都在主 actor 上运行。await 关键字会挂起任务并释放线程,但任务恢复时会回到同一个隔离域(主 actor)。除非被等待的 API(例如 URLSession)在内部处理,否则不会涉及后台线程。

适用任务的场景:

  • 高延迟操作(网络、文件 I/O)
  • 库 API 会为你处理后台工作(URLSession、FileManager)
  • 你自己的代码仍在主线程上运行

步骤 3:并发代码(后台线程)

当CPU 密集型工作阻塞 UI 时,加入并发。

问题:解码会阻塞 UI

性能分析显示,decodeImage() 耗时 200ms,导致 UI 卡顿:

func fetchAndDisplayImage(url: URL) async throws {
    let (data, _) = try await URLSession.shared.data(from: url)
    let image = decodeImage(data)  // ❌ 200ms on main thread!
    view.displayImage(image)
}
解决方案 1:@concurrent 属性(Swift 6.2+)

强制函数始终在后台线程上运行:

func fetchAndDisplayImage(url: URL) async throws {
    let (data, _) = try await URLSession.shared.data(from: url)
    let image = await decodeImage(data)  // ✅ Runs on background thread
    view.displayImage(image)
}

@concurrent
func decodeImage(_ data: Data) async -> Image {
    // ✅ Always runs on background thread pool
    // Good for: image processing, file I/O, parsing
    return Image()
}

@concurrent 的作用:

  • 函数始终会切换到后台线程池
  • 编译器会突出显示对主 actor 数据的访问(指出需要修复的地方)
  • 不使用 await 就无法访问 @MainActor 属性

要求:Swift 6.2+、Xcode 26+、iOS 26+

解决方案 2:nonisolated(库 API)

如果要提供通用 API,则改用 nonisolated:

// ✅ Stays on caller's actor
nonisolated
func decodeImage(_ data: Data) -> Image {
    // Runs on whatever actor called it
    // Main actor → stays on main actor
    // Background → stays on background
    return Image()
}

适用 nonisolated 的场景:

  • 由调用方决定在哪里执行工作的库 API
  • 可以在主线程上执行的小型操作
  • 在多种上下文中使用的通用代码

适用 @concurrent 的场景:

  • 应始终在后台运行的操作(图像处理、解析)
  • 不应阻塞 UI 的性能关键型工作
解除与主 actor 的关联

将函数标记为 @concurrent 后,编译器会显示对主 actor 的访问:

@MainActor
class ImageModel {
    var cachedImage: [URL: Image] = [:]  // Main actor data

    @concurrent
    func decodeImage(_ data: Data, at url: URL) async -> Image {
        if let image = cachedImage[url] {  // ❌ Error: main actor access!
            return image
        }
        // decode...
    }
}
策略 1:移至调用方
func fetchAndDisplayImage(url: URL) async throws {
    // ✅ Check cache on main actor BEFORE async work
    if let image = cachedImage[url] {
        view.displayImage(image)
        return
    }

    let (data, _) = try await URLSession.shared.data(from: url)
    let image = await decodeImage(data)  // No URL needed now
    view.displayImage(image)
}

@concurrent
func decodeImage(_ data: Data) async -> Image {
    // ✅ No main actor access needed
    return Image()
}
策略 2:通过 @MainActor 辅助方法访问
@MainActor
func getCachedImage(for url: URL) -> Image? {
    cachedImage[url]
}

@concurrent
func decodeImage(_ data: Data, at url: URL) async -> Image {
    // ✅ Call @MainActor function to access isolated state
    if let image = await getCachedImage(for: url) {
        return image
    }
    // decode...
}
策略 3:设为 nonisolated
nonisolated
func decodeImage(_ data: Data) -> Image {
    // ✅ No actor isolation, can call from anywhere
    return Image()
}
并发线程池

工作在后台运行时:

Main Thread:    [UI] → (suspend) → [UI Update]
                         ↓
Background Pool: [Task A] → [Task B] → [Task A resumes]
                 Thread 1    Thread 2    Thread 3

要点:

  • 系统会管理线程池大小(Watch 上为 1-2 个线程,Mac 上则有多个线程)
  • 任务恢复时可能运行在与启动时不同的线程上
  • 你无需指定线程——系统会自动优化

步骤 4:Actor(将数据移出主线程)

当太多代码在主 actor 上运行并造成争用时,加入 actor。

问题:主 actor 争用
@MainActor
class ImageModel {
    var cachedImage: [URL: Image] = [:]
    let networkManager: NetworkManager = NetworkManager()  // ❌ Also @MainActor

    func fetchAndDisplayImage(url: URL) async throws {
        // ✅ Background work...
        let connection = await networkManager.openConnection(for: url)  // ❌ Hops to main!
        let data = try await connection.data(from: url)
        await networkManager.closeConnection(connection, for: url)  // ❌ Hops to main!

        let image = await decodeImage(data)
        view.displayImage(image)
    }
}

问题:后台任务频繁切换到主 actor 以访问网络管理器。

解决方案:网络管理器 Actor
// ✅ Move network state off main actor
actor NetworkManager {
    var openConnections: [URL: Connection] = [:]

    func openConnection(for url: URL) -> Connection {
        if let connection = openConnections[url] {
            return connection
        }
        let connection = Connection()
        openConnections[url] = connection
        return connection
    }

    func closeConnection(_ connection: Connection, for url: URL) {
        openConnections.removeValue(forKey: url)
    }
}

@MainActor
class ImageModel {
    let networkManager: NetworkManager = NetworkManager()

    func fetchAndDisplayImage(url: URL) async throws {
        // ✅ Now runs mostly on background
        let connection = await networkManager.openConnection(for: url)
        let data = try await connection.data(from: url)
        await networkManager.closeConnection(connection, for: url)

        let image = await decodeImage(data)
        view.displayImage(image)
    }
}

变更内容:

  • NetworkManager 现在是 actor,而不是 @MainActor class
  • 网络状态隔离在其自身的 actor 中
  • 后台代码可以访问网络管理器,而无需切换到主 actor
  • 主线程得以释放,可用于 UI 工作
何时使用 Actor

✅ 适合使用 actor 的场景:

  • 具有独立状态的非 UI 子系统(网络管理器、缓存、数据库)
  • 导致主 actor 争用的数据
  • 将 UI 代码中的关注点分离

❌ 不要将 actor 用于:

  • 面向 UI 的类(ViewModels、View Controllers)→ 使用 @MainActor
  • UI 使用的模型类 → 保持为 @MainActor 或非 Sendable
  • 应用中的每个类(actor 会增加复杂度)

指南:先进行性能分析。如果主 actor 状态过多并造成瓶颈,请一次将一个子系统提取到 actor 中。


Sendable 类型(跨 actor 边界传递的数据)

当数据在 actor 或任务之间传递时,Swift 会检查它是否为 Sendable(可安全共享)。

值类型可发送
// ✅ Value types copy when passed
let url = URL(string: "https://swift.org")!

Task {
    // ✅ This is a COPY of url, not the original
    // URLSession.shared.data runs on background automatically
    let data = try await URLSession.shared.data(from: url)
}

// ✅ Original url unchanged by background task

为什么安全:每个 Actor 都有自己的独立副本。更改不会影响其他副本。

什么是可发送类型?
// ✅ Basic types
extension URL: Sendable {}
extension String: Sendable {}
extension Int: Sendable {}
extension Date: Sendable {}

// ✅ Collections of Sendable elements
extension Array: Sendable where Element: Sendable {}
extension Dictionary: Sendable where Key: Sendable, Value: Sendable {}

// ✅ Structs/enums with Sendable storage
struct Track: Sendable {
    let id: String
    let title: String
    let duration: TimeInterval
}

enum PlaybackState: Sendable {
    case stopped
    case playing
    case paused
}

// ✅ Main actor types
@MainActor class ImageModel {}  // Implicitly Sendable (actor protects state)

// ✅ Actor types
actor NetworkManager {}  // Implicitly Sendable (actor protects state)
引用类型(类)与可发送性
// ❌ Classes are NOT Sendable by default
class MyImage {
    var width: Int
    var height: Int
    var pixels: [Color]

    func scale(by factor: Double) {
        // Mutates shared state
    }
}

let image = MyImage()
let otherImage = image  // ✅ Both reference SAME object

image.scale(by: 0.5)  // ✅ Changes visible through otherImage!

并发问题:

func scaleAndDisplay(imageName: String) {
    let image = loadImage(imageName)

    Task {
        image.scale(by: 0.5)  // Background task modifying
    }

    view.displayImage(image)  // Main thread reading
    // ❌ DATA RACE! Both threads could touch same object!
}
解决方案 1:发送前完成修改
@concurrent
func scaleAndDisplay(imageName: String) async {
    let image = loadImage(imageName)
    image.scale(by: 0.5)  // ✅ All modifications on background
    image.applyAnotherEffect()  // ✅ Still on background

    await view.displayImage(image)  // ✅ Send to main actor AFTER modifications done
    // ✅ Main actor now owns image exclusively
}
解决方案 2:不要在并发任务间共享类

将模型类设为 @MainActor 或不可发送,以防止并发访问。

可发送性检查

以下情况会自动检查:

  • 向 Actor 传入数据或从 Actor 传出数据
  • 向任务传入数据或从任务传出数据
  • 使用 await 跨越 Actor 边界
func fetchAndDisplayImage(url: URL) async throws {
    let (data, _) = try await URLSession.shared.data(from: url)
    //  ↑ Sendable    ↑ Sendable (crosses to background)

    let image = await decodeImage(data)
    //          ↑ data crosses to background (must be Sendable)
    //                            ↑ image returns to main (must be Sendable)
}

常见模式(复制粘贴模板)

模式 1:可发送的枚举/结构体

适用场景:类型跨越 Actor 边界

// ✅ Enum (no associated values)
private enum PlaybackState: Sendable {
    case stopped
    case playing
    case paused
}

// ✅ Struct (all properties Sendable)
struct Track: Sendable {
    let id: String
    let title: String
    let artist: String?
}

// ✅ Enum with Sendable associated values
enum FetchResult: Sendable {
    case success(data: Data)
    case failure(error: Error)  // Error is Sendable
}

模式 2:委托方法中捕获值(至关重要)

适用场景:nonisolated 委托方法需要更新 @MainActor 状态

nonisolated func delegate(_ param: SomeType) {
    // ✅ Step 1: Capture delegate parameter values BEFORE Task
    let value = param.value
    let status = param.status

    // ✅ Step 2: Task hop to MainActor
    Task { @MainActor in
        // ✅ Step 3: Safe to access self (we're on MainActor)
        self.property = value
        print("Status: \(status)")
    }
}

原因:委托方法是 nonisolated 的(由库的线程调用)。在 Task 前捕获参数。在 Task { @MainActor in } 中访问 self 是安全的。


模式 3:在任务中使用弱 Self

适用场景:Task 被存储为属性,或运行时间较长

class MusicPlayer {
    private var progressTask: Task<Void, Never>?

    func startMonitoring() {
        progressTask = Task { [weak self] in  // ✅ Weak capture
            guard let self = self else { return }

            while !Task.isCancelled {
                await self.updateProgress()
            }
        }
    }

    deinit {
        progressTask?.cancel()
    }
}

注意:短时运行的 Task(未存储)可以使用强捕获。


模式 4:使用 @concurrent 执行后台工作

适用场景:CPU 密集型工作应始终在后台运行(Swift 6.2+)

@concurrent
func decodeImage(_ data: Data) async -> Image {
    // ✅ Always runs on background thread pool
    // Good for: image processing, file I/O, JSON parsing
    return Image()
}

// Usage
let image = await decodeImage(data)  // Automatically offloads

要求:Swift 6.2+、Xcode 26+、iOS 26+


模式 5:隔离的协议一致性(Swift 6.2+)

适用场景:类型需要遵循具有特定 Actor 隔离要求的协议

protocol Exportable {
    func export()
}

class PhotoProcessor {
    @MainActor
    func exportAsPNG() {
        // Export logic requiring UI access
    }
}

// ✅ Conform with explicit isolation
extension StickerModel: @MainActor Exportable {
    func export() {
        photoProcessor.exportAsPNG()  // ✅ Safe: both on MainActor
    }
}

使用时机:协议方法需要特定的 Actor 上下文(UI 使用主 Actor,处理工作使用后台)


模式 6:为不可发送类型捕获 #isolation(SE-0420)

适用场景:Task 闭包需要使用不可发送的委托或对象

func process(
    delegate: NonSendableDelegate,
    isolation: isolated (any Actor)? = #isolation
) {
    Task {
        _ = isolation  // Forces capture — Task inherits caller's isolation
        delegate.doWork()  // ✅ Safe: running on caller's actor
    }
}

为什么需要 _ = isolation:根据 SE-0420,只有当闭包捕获了隔离参数的非可选绑定时,Task 闭包才会继承隔离。缺少这一行时,Task 会在默认执行器上运行,并且不可发送的捕获会导致错误。

使用时机:启动需要使用不可发送委托对象的 Task、需要访问调用方状态的即发即弃异步工作,或将基于回调的 API 转换为异步流。


模式 7:Task 超时

适用场景:异步操作必须在截止时间前完成

func withTimeout<T: Sendable>(
    _ duration: Duration,
    operation: @Sendable @escaping () async throws -> T
) async throws -> T {
    try await withThrowingTaskGroup(of: T.self) { group in
        group.addTask { try await operation() }
        group.addTask {
            try await Task.sleep(for: duration)
            throw TimeoutError()
        }
        guard let result = try await group.next() else {
            throw TimeoutError()
        }
        group.cancelAll()  // Cancel the loser (critical — without this it keeps running)
        return result
    }
}

// Usage
let data = try await withTimeout(.seconds(5)) {
    try await slowNetworkRequest()
}

模式 8:将 UI 代码设为 MainActor

适用场景:代码涉及 UI

@MainActor
class PlayerViewModel: ObservableObject {
    @Published var currentTrack: Track?
    @Published var isPlaying: Bool = false

    func play(_ track: Track) async {
        // Already on MainActor
        self.currentTrack = track
        self.isPlaying = true
    }
}

数据持久化并发模式

模式 9:后台访问 SwiftData
actor DataFetcher {
    let modelContainer: ModelContainer

    func fetchAllTracks() async throws -> [Track] {
        let context = ModelContext(modelContainer)
        let descriptor = FetchDescriptor<Track>(
            sortBy: [SortDescriptor(\.title)]
        )
        return try context.fetch(descriptor)
    }
}

@MainActor
class TrackViewModel: ObservableObject {
    @Published var tracks: [Track] = []

    func loadTracks() async {
        let fetchedTracks = try await fetcher.fetchAllTracks()
        self.tracks = fetchedTracks  // Back on MainActor
    }
}
模式 10:线程安全地获取 Core Data 数据
actor CoreDataFetcher {
    func fetchTracksID(genre: String) async throws -> [String] {
        let context = persistentContainer.newBackgroundContext()
        var trackIDs: [String] = []

        try await context.perform {
            let request = NSFetchRequest<CDTrack>(entityName: "Track")
            request.predicate = NSPredicate(format: "genre = %@", genre)
            let results = try context.fetch(request)
            trackIDs = results.map { $0.id }  // Extract IDs before leaving context
        }

        return trackIDs  // Lightweight, Sendable
    }
}
模式 11:带进度显示的批量导入
actor DataImporter {
    func importRecords(_ records: [RawRecord], onProgress: @Sendable @MainActor (Int, Int) -> Void) async throws {
        let chunkSize = 1000
        let context = ModelContext(modelContainer)

        for (index, chunk) in records.chunked(into: chunkSize).enumerated() {
            for record in chunk {
                context.insert(Track(from: record))
            }
            try context.save()

            let processed = (index + 1) * chunkSize
            await onProgress(min(processed, records.count), records.count)

            if Task.isCancelled { throw CancellationError() }
        }
    }
}
模式 12:在后台执行 GRDB 操作
actor DatabaseQueryExecutor {
    let dbQueue: DatabaseQueue

    func fetchUserWithPosts(userId: String) async throws -> (user: User, posts: [Post]) {
        return try await dbQueue.read { db in
            let user = try User.filter(Column("id") == userId).fetchOne(db)!
            let posts = try Post
                .filter(Column("userId") == userId)
                .order(Column("createdAt").desc)
                .limit(100)
                .fetchAll(db)
            return (user, posts)
        }
    }
}

快速决策树

Starting new feature?
└─ Is UI responsive with all operations on main thread?
   ├─ YES → Stay single-threaded (Step 1)
   └─ NO → Continue...
       └─ Do you have high-latency operations? (network, file I/O)
          ├─ YES → Add async/await (Step 2)
          └─ NO → Continue...
              └─ Do you have CPU-intensive work? (Instruments shows main thread busy)
                 ├─ YES → Add @concurrent or nonisolated (Step 3)
                 └─ NO → Continue...
                     └─ Is main actor contention causing slowdowns?
                        └─ YES → Extract subsystem to actor (Step 4)

Error: "Main actor-isolated property accessed from nonisolated context"
├─ In delegate method?
│  └─ Pattern 2: Value Capture Before Task
├─ In async function?
│  └─ Add @MainActor or call from Task { @MainActor in }
└─ In @concurrent function?
   └─ Move access to caller, use await, or make nonisolated

Error: "Main actor-isolated property can not be referenced from a Sendable closure"
├─ Read-only access?
│  └─ Capture the value: { [count] in print(count) }
├─ Need mutation after background work?
│  └─ Extract background work to @concurrent func, call from @MainActor context:
│     func upload(_ data: Data) { Task { await doUpload(data); self.count += 1 } }
│     @concurrent func doUpload(_ data: Data) async { /* heavy work */ }
├─ Need mutation only?
│  └─ Task { @MainActor in self.count += 1 }
└─ Own the API?
   └─ Make closure parameter @MainActor: (_ closure: @Sendable @MainActor () -> Void)

Error: "Capture of non-sendable type in @Sendable closure"
├─ Type is a struct?
│  └─ Add `: Sendable` (all stored properties must be Sendable)
├─ Type is a class you own?
│  └─ Convert to actor, or make final + immutable + Sendable
└─ Migration staging?
   └─ @unchecked Sendable temporarily, track removal ticket

Error: "Value of non-Sendable type accessed after being transferred"
├─ Can avoid concurrent access?
│  └─ Capture as let: Task { [myArray] in ... }
├─ Need shared mutable access?
│  └─ Wrap in actor
└─ Redesign possible?
   └─ Rework to avoid sharing mutable state across tasks

Error: "Reference to captured var in concurrently-executing code"
├─ Variable doesn't need to be mutable?
│  └─ Change var to let
├─ Needs to stay var?
│  └─ Capture in closure: { [task] in ... } or shadow: let t = task
└─ In a loop?
   └─ Create let binding inside loop body before Task

Error: "Type does not conform to Sendable"
├─ Enum/struct with Sendable properties?
│  └─ Add `: Sendable`
└─ Class?
   └─ Make @MainActor or keep non-Sendable (don't share concurrently)

Want to offload work to background?
├─ Always background (image processing)?
│  └─ Use @concurrent (Swift 6.2+)
├─ Caller decides?
│  └─ Use nonisolated
└─ Too much main actor state?
   └─ Extract to actor

构建设置(Xcode 16+)

Build Settings → Swift Compiler — Language
→ "Default Actor Isolation" = Main Actor
→ "Approachable Concurrency" = Yes

Build Settings → Swift Compiler — Concurrency
→ "Strict Concurrency Checking" = Complete

Swift Package Manager 对应设置:

.target(
    name: "MyTarget",
    swiftSettings: [
        .swiftLanguageMode(.v6),
        .defaultIsolation(MainActor.self)
    ]
)

启用的功能:

  • 主 Actor 模式(默认将所有代码设为 @MainActor)
  • 编译时防止数据竞争
  • 渐进式采用并发

反模式与错误辩解

错误辩解为什么不对应该这样做
“我用 Task.detached 就能让它在后台运行”Task.detached 会丢失 Actor 上下文、优先级和任务局部值。这通常不是你想要的。使用 @concurrent(Swift 6.2+)强制在后台运行。需要继承 Actor 时使用 Task {}。
“我加上 @unchecked Sendable 就能消除这个问题”你是在向编译器隐瞒数据竞争,程序会在生产环境中崩溃。让类型真正符合 Sendable(使用 struct/enum)、使用 Actor,或使用 sending 参数。
“我用 nonisolated(unsafe) 就能解决这个问题”它不会提供任何运行时保护。编译器会停止检查,数据竞争也就检测不到了。使用正确的隔离方式(@MainActor、Actor、Mutex)。仅将其用于全局常量。
“这个异步函数会在后台线程运行”async 会挂起而不阻塞,但恢复时仍在同一个 Actor上。@MainActor 异步函数会在主线程上运行。使用 @concurrent 强制在后台运行。不要认为 async 就等于后台运行。
“我用 DispatchQueue.global().async 把它包装起来”在结构化并发中通过 GCD 切换队列会破坏隔离保证,还有导致线程数量激增的风险。使用 @concurrent,或将代码提取到 Actor 中。仅在桥接层使用 GCD。
“每个类都应该是 Actor”Actor 会增加串行化开销。自定义 Actor 上的 UI 代码无法更新视图。UI/ViewModel 代码使用 @MainActor。Actor 仅用于非 UI 的共享可变状态。
“我用 @preconcurrency 就能更快发布”你假定了编译器无法验证的线程安全性,生产环境中会出现崩溃。迁移到正确的并发实现。仅将 @preconcurrency 作为临时桥接,并创建移除任务。
“这个功能需要并发”并发会增加复杂性。大多数应用代码在 MainActor 上以单线程运行也能正常工作。先进行性能分析。仅当 Instruments 显示存在瓶颈时才升级。
“每个 Task 都对应一个线程”多个 Task 会共享一个协作式线程池。挂起后,Task 可以在任意线程上恢复。从隔离域而非线程的角度思考。
“我给每项工作都创建一个 Task”非结构化 Task 会失去取消传播和错误处理能力,也更难推理。对相关并行工作使用 async let(固定数量)或 TaskGroup(动态数量)。仅在同步→异步桥接时使用 Task {}。
❌ 在不需要时使用并发
// ❌ Premature optimization
@concurrent
func addNumbers(_ a: Int, _ b: Int) async -> Int {
    return a + b  // ❌ Trivial work, concurrency adds overhead
}

// ✅ Keep simple
func addNumbers(_ a: Int, _ b: Int) -> Int {
    return a + b
}
❌ 在存储的 Task 中强引用 self
// ❌ Memory leak — task retains self, self retains task
progressTask = Task {
    while true {
        await self.update()  // ❌ Strong capture in infinite loop
    }
}

// ✅ Weak capture breaks the cycle
progressTask = Task { [weak self] in
    while let self, !Task.isCancelled {
        await self.updateProgress()
    }
}

经验法则:对于能快速完成的短期 Task,强引用 self 没问题。如果 Task 被存储为属性,或会无限期运行(循环、异步序列),请使用 [weak self]。

❌ 让每个类都成为 Actor
// ❌ Don't do this
actor MyViewModel: ObservableObject {  // ❌ UI code should be @MainActor!
    @Published var state: State  // ❌ Won't work correctly
}

// ✅ Do this
@MainActor
class MyViewModel: ObservableObject {
    @Published var state: State
}
❌ 在异步上下文中使用 Thread.current
// ❌ Swift 6 language mode: compiler error
func checkThread() async {
    print(Thread.current)  // ERROR: unavailable from asynchronous contexts
}

// ✅ If you must inspect threads (debugging only)
extension Thread {
    static var currentThread: Thread { Thread.current }
}
print(Thread.currentThread)  // Works, but don't rely on thread identity

原因:Task 会在挂起点之间切换线程。在 Swift Concurrency 中,线程标识没有意义——应从隔离域的角度进行推理。


代码审查清单

添加并发之前
  • 已进行性能分析,并确认 UI 无响应
  • 已找出具体的慢操作(网络、CPU、竞争)
  • 从最简单的解决方案开始(async → concurrent → Actor)
Async/Await
  • 仅用于高延迟操作
  • 在事件触发时创建 Task
  • 使用 do-catch 处理错误
后台工作
  • 始终在后台运行的工作使用 @concurrent(Swift 6.2+)
  • 库 API 使用 nonisolated
  • 不在主 Actor 上执行阻塞操作
Sendable
  • 跨 Actor 传递的数据使用值类型
  • 类应保持为 @MainActor 或非 Sendable
  • 不并发修改共享类
Actor
  • 仅用于非 UI 子系统
  • UI 代码保持为 @MainActor
  • Model 类应保持为 @MainActor 或非 Sendable

迁移习惯

将现有代码库迁移到严格并发时:

  1. 分小批次迭代 — 一次修复一个模块或一类诊断。重新构建、测试、提交。不要批量处理。
  2. 从一开始就将新类型设计为 Sendable — 为现有类型补充 Sendable 会波及整个代码库。
  3. 尽早设置默认隔离 — 对应用模块,将 @MainActor 设为默认隔离。这可以消除大多数误报。
  4. 避免范围蔓延 — 并发迁移 PR 应仅包含并发相关改动,不要夹带架构重构。
  5. 不要为了更快发布而屏蔽警告 — @unchecked Sendable、nonisolated(unsafe) 和 @preconcurrency 是迁移工具,不是永久解决方案。为每一项都创建一个待移除任务。

资源

WWDC:2025-268、2025-245、2022-110351、2021-10133

文档:/swift/adoptingswift6、/swift/sendable

技能:axiom-lldb(在调试器中调试 actor/task 状态)


最后更新:2025-12-01 状态:已结合 WWDC 2025-268 的渐进式实践路径、@concurrent 属性、隔离的一致性 conformance,以及易于上手的并发模式进行增强

评论

支持 Markdown