关于 axiom-swift-concurrency
面向 Swift 开发者的并发指南,按需讲解从异步到并发与 Actor 的演进,并帮助诊断 Swift 6 并发安全问题。重点是以隔离域而非线程来规划工作。
主要功能
- 01循序介绍并发演进
- 02讲解 async/await 与任务
- 03说明 @MainActor、Actor 与隔离
- 04指导 Sendable 与数据竞争排查
- 05提供后台 CPU 工作模式
适用场景
- 01规划新 Swift 项目的并发策略
- 02排查 Actor 隔离或 Sendable 错误
- 03将委托回调改为异步模式
- 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
何时进阶:
- 保持单线程,如果 UI 响应流畅且操作很快
- 添加 async/await,当高延迟操作(网络、文件 I/O)会阻塞 UI 时
- 添加并发,当 CPU 密集型工作(图像处理、解析)会导致 UI 卡顿时
- 添加 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)
}
会发生什么:
- 函数在主线程上启动
await会挂起函数,但不会阻塞主线程- URLSession 在后台线程上获取数据(由库处理)
- 数据到达后,函数会在主线程上恢复执行
- 整个过程中,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
迁移习惯
将现有代码库迁移到严格并发时:
- 分小批次迭代 — 一次修复一个模块或一类诊断。重新构建、测试、提交。不要批量处理。
- 从一开始就将新类型设计为 Sendable — 为现有类型补充 Sendable 会波及整个代码库。
- 尽早设置默认隔离 — 对应用模块,将
@MainActor设为默认隔离。这可以消除大多数误报。 - 避免范围蔓延 — 并发迁移 PR 应仅包含并发相关改动,不要夹带架构重构。
- 不要为了更快发布而屏蔽警告 —
@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,以及易于上手的并发模式进行增强