Actor 的核心不是“内部自带一把锁”,而是让可变状态属于一个编译器可检查的隔离域:只有运行在该 Actor 上的代码可以直接访问隔离状态,跨域调用必须经过异步边界,传递的值还要满足
Sendable等安全契约。Actor 可以消除大量 Data Race,却不保证一个async方法从头到尾原子执行;每个await都可能允许其他任务进入并改变状态。工程上必须同时设计隔离边界与业务不变量,否则代码虽然通过 Swift 6 严格并发检查,仍会发生旧请求覆盖、新账号读到旧缓存或重复提交。
一、本文解决什么问题
Swift 6 工程中经常遇到这些问题:
- Actor 是否等价于 Serial Dispatch Queue 或 Mutex?
- Actor 方法是否固定在同一线程执行?
- 为什么跨 Actor 调用一个没有写
async的方法仍要await? - Actor 能否保证
await前读取的状态在恢复后仍有效? @MainActor是否只用于 View Controller,网络和 ViewModel 应如何分层?MainActor.run、Task { @MainActor in }和给类型标@MainActor有什么差别?- 多个数据库 Service 如何共享一个 Global Actor,而不是各自创建 Actor?
- 什么是 Isolation Domain,值跨域时为什么需要
Sendable? Sendable是否意味着对象绝对不可变或内部没有 Lock?- 为什么
@unchecked Sendable能消除编译错误,却可能留下真实 Data Race? nonisolated能访问哪些成员,何时会破坏封装?- Objective-C Delegate 没有并发标注、回调线程不固定时,如何进入 MainActor?
- Swift 5 Language Mode 只有 Warning,Swift 6 为什么变成 Error?
@preconcurrency import是兼容手段,还是线程安全修复?- Xcode 26/Swift 6.2 的 Default Actor Isolation 设置为何会改变同一段源码的诊断?
这些问题需要同时区分三层安全:
- Memory Safety / Data Race Safety:是否存在未同步的并发读写;
- Isolation Correctness:代码是否在正确 Actor/Global Actor 上访问状态;
- Business Consistency:跨
await后结果是否仍属于当前请求、账号和状态版本。
本文以 Swift 6 Language Mode、Complete Strict Concurrency、iOS 17+ 为主要验证基线,示例按 Xcode 26.1.1、Apple Swift 6.2.1 与 iOS Simulator SDK 编写。Swift Concurrency 的 Executor、Job Queue、Priority Escalation、Region-based Isolation 分析和 MainActor 与 Main Thread 的底层调度细节会随 Toolchain 演进;文章依赖的是语言公开隔离语义。编译诊断还受 Swift Language Mode、SWIFT_STRICT_CONCURRENCY、Default Actor Isolation、Upcoming Features、SDK Annotation 与第三方模块标注影响,迁移时必须记录实际 Build Settings。
核心结论
- Actor 是引用类型和隔离域。Actor-isolated Mutable State 只能由该 Actor 上的代码直接访问;外部调用经过 Actor Executor 调度,不能把 Actor 理解为固定线程。
- Actor 同一时刻不会并发执行两个 Actor-isolated Synchronous Fragment,但
await会把方法切成多个可重入片段。Actor 防 Data Race,不保证整个 Async Method 原子。 - 跨 Actor 调用即使目标方法声明中没有
async,调用点也通常需要await,因为调用方必须异步等待目标 Actor 获得执行机会。 - MainActor 是标准 Global Actor,用于隔离 UI 和其他主执行域状态。Async MainActor Code 由 MainActor Executor 调度;不要用
Thread.isMainThread替代编译期隔离契约。 - 给整个 UI Model 标
@MainActor通常比零散给 Setter 加标注更清楚,但同步 CPU 重任务若写在其隔离方法中仍会阻塞主执行环境;网络 Await 本身通常不会在等待期间占用 Main Thread。 MainActor.run同步地执行一个 MainActor-isolated Async Context 片段并等待结果;Task { @MainActor in }创建新的 Unstructured Task,调用方不自动等待,错误、取消和顺序语义不同。- Custom Global Actor 让多个类型共享同一个语义隔离域,适合数据库/特定旧 SDK 等必须串行访问的系统边界。不要为所有业务创建一个巨大全局 Actor。
- Isolation Domain 可以是 Actor Instance、Global Actor、Nonisolated Context 或 Task-owned Region。跨域传递值必须证明不会产生并发别名,通常通过
Sendable、Actor Reference 或编译器 Region Analysis 完成。 Sendable表示值可以安全跨并发域传递,不等于 Deeply Immutable。Value Type 通常要求成员 Sendable;Reference Type 可通过不可变状态、内部同步或 Actor Isolation 达成。- Actor Type 本身可安全作为引用跨域传递,因为其隔离状态仍需通过 Actor Boundary 访问;这不意味着内部属性可以从外部直接读写。
@SendableClosure 限制捕获值必须可安全跨域。捕获 UIView、Mutable Class 或未隔离变量通常会在 Swift 6 报错,而不是靠 Weak Capture 自动变安全。@unchecked Sendable是开发者对编译器的人工承诺,不添加 Lock、Copy 或 Runtime Check。只有经过审计且所有可变状态受同步保护时才能使用。- Actor Reentrancy 要求
await后重新验证 Version、Identity、Authorization 和 Invariant。典型修复包括 Revision、In-flight Task Coalescing、两阶段提交或把不可分割状态变更留在无await的隔离片段。 nonisolated成员不需要 Actor Hop,适合只依赖 Sendable Immutable Data 的协议要求、日志标识和纯计算。它不能无保护访问 Actor-isolated Mutable State。- Objective-C Callback 的 Queue/Thread 与调用次数必须按 API 文档处理。对任意线程回调使用
nonisolatedAdapter,复制/验证 Sendable Input 后显式 Hop 到 MainActor/Actor;Task { @MainActor in }会改变为异步调度。 MainActor.assumeIsolated只适合外部契约已保证当前就在 MainActor、但编译器无法表达的窄边界;错误假设可能触发 Runtime Failure,不能用来绕过未知线程 Callback。- Swift 5 Mode 的 Warning 或
@preconcurrency抑制不代表代码安全。Swift 6 将更多潜在 Data Race 提升为 Error;迁移应先画状态所有权和隔离边界,再处理诊断。 - 编译器能证明 Data Race Safety,但不能证明业务时序正确。Actor 中的余额检查、Token 刷新、页面请求等跨 Await 流程仍需要状态机和并发测试。
二、Actor 隔离的执行模型
Actor 保证的是 Actor-isolated Fragment 不并发重叠访问状态,而不是:
- 每个 Actor 独占一条 Thread;
- 方法按调用墙钟时间严格 FIFO 完成;
- Async Method 从入口到 Return 不可插入其他任务;
- 自动取消重复请求;
- 自动维护业务事务。
Runtime 可以在不同 Thread 上执行同一 Actor 的不同片段。隔离正确性应由类型系统表达,不依赖 Thread ID 或 Queue Label。
三、Actor:把可变状态放进明确所有者
3.1 基本 Actor
actor InventoryStore {
private var quantities: [UUID: Int] = [:]
func quantity(for productID: UUID) -> Int {
quantities[productID, default: 0]
}
func restock(productID: UUID, amount: Int) throws {
guard amount > 0 else { throw InventoryError.invalidAmount }
quantities[productID, default: 0] += amount
}
}外部访问:
let current = await inventory.quantity(for: productID)
try await inventory.restock(productID: productID, amount: 5)quantity 和 restock 的声明不含 async,但跨 Actor 调用需要等待其隔离域可执行,因此调用点是 await。若从 InventoryStore 的另一个 Isolated Method 调用,则已在同一隔离域,不需要 Hop。
3.2 Actor Reference 与状态
Actor Reference 可以在 Task/Actor 之间共享,安全性来自访问受隔离,并非复制 Actor。不要暴露内部 Mutable Reference:
// 高风险设计:返回的可变引用可能逃出隔离域。
actor BadStore {
private var buffer = NSMutableData()
func rawBuffer() -> NSMutableData { buffer }
}在 Complete Strict Concurrency 下,这类 Non-Sendable Result 跨边界会受到诊断。修复是返回 Sendable Value Snapshot、在 Actor 内完成操作,或用独立同步类型明确所有权。
3.3 Actor 不是数据库事务
Actor 只串行化进程内隔离片段,不能替代:
- 数据库 Transaction/Constraint;
- 跨进程 File Lock;
- Server Idempotency;
- 多设备一致性;
- Durable Storage。
库存 Actor 的内存检查与 Server 扣减之间存在 Await 时,仍需要服务端原子性。
四、MainActor:UI 与主执行域隔离
4.1 类型级 MainActor
@MainActor
final class ProfileViewModel {
enum State {
case idle
case loading
case loaded(Profile)
case failed(String)
}
private(set) var state: State = .idle
private var loadTask: Task<Void, Never>?
func load(userID: UUID) {
loadTask?.cancel()
state = .loading
loadTask = Task { [weak self] in
guard let self else { return }
do {
let profile = try await repository.profile(id: userID)
try Task.checkCancellation()
state = .loaded(profile)
} catch is CancellationError {
return
} catch {
state = .failed(error.localizedDescription)
}
}
}
deinit {
loadTask?.cancel()
}
}整个类型隔离后,调用方、Closure 和协议实现的要求更明确。Task 在 MainActor Context 中创建,继承 MainActor Isolation;网络请求 Await 期间 Actor 可执行其他任务。Repository 的同步重 CPU 工作不能偷偷写在 MainActor 方法里。
4.2 MainActor.run
let profile = try await repository.profile(id: userID)
await MainActor.run {
viewModel.apply(profile)
}调用方 Await MainActor Closure 完成后再继续,适合从 Nonisolated Async Context 提交一个短 UI 片段。若大量操作都要反复 MainActor.run,通常说明类型/方法应该直接声明 @MainActor。
4.3 Task { @MainActor in }
Task { @MainActor [weak viewController] in
viewController?.show(result)
}它创建一个新的 Unstructured Task:调用点立即继续,错误和取消不自动回传,多个 Callback 的任务顺序也不应被当作事务保证。只用于 Callback Adapter 等边界,并由生命周期/Identity 防晚到。
4.4 MainActor 与 Main Thread
对现代 Async Swift Concurrency Code,MainActor 是主执行隔离契约,UIKit/UI 工作应依赖它。仍需理解边界:
- 旧 Swift 5 同步调用链可能缺少完整隔离检查;
- Objective-C Runtime 不理解 Swift Actor Annotation;
@MainActor同步 Closure 被错误地从未检查旧 API 调用时,诊断取决于 Annotation/Language Mode;- 不应因为
Thread.isMainThread == true就认为当前拥有 MainActor Isolation; - 也不应通过
DispatchQueue.main.async到处代替类型级 MainActor 设计。
五、Global Actor:多个类型共享一个隔离域
5.1 定义 Custom Global Actor
@globalActor
actor DatabaseActor {
static let shared = DatabaseActor()
}
@DatabaseActor
final class UserRecordStore {
private var records: [UUID: UserRecord] = [:]
func record(id: UUID) -> UserRecord? {
records[id]
}
func save(_ record: UserRecord) {
records[record.id] = record
}
}
@DatabaseActor
struct DatabaseMigration {
static func runIfNeeded() throws {
// Access the same database isolation domain.
}
}两个类型标同一 Global Actor 后可以在该域内同步协作,外部调用需要 Await。
5.2 Global Actor 的适用范围
适合:
- 一个不支持并发访问的数据库连接/旧 SDK;
- 多个类型必须共享同一串行域;
- 跨类型协议明确要求同一 Isolation。
不适合:
- 把整个 Data Layer 全放一个 Actor,导致无关业务互相阻塞;
- 用 Global Actor 掩盖 Repository 的线程安全问题;
- 在 Global Actor 内执行长同步 I/O/CPU;
- 把跨进程一致性误当进程内串行。
5.3 MainActor 是 Global Actor
MainActor 是语言/标准库提供的 Global Actor。@MainActor 类型之间共享同一主隔离域,而不是每个 ViewModel 拥有独立 Actor Queue。
六、Isolation Domain:谁可以同步访问这份状态
6.1 Actor Instance 是不同隔离域
let first = InventoryStore()
let second = InventoryStore()两个 Actor Instance 各自隔离,可以并发执行。Actor Type 相同不代表共享同一 Serial Domain;需要共享域时传同一 Instance 或使用 Global Actor。
6.2 隔离成员分类
- Actor Stored Mutable Property:Actor-isolated;
- Actor Instance Method:默认 Actor-isolated;
@MainActor/Custom Global Actor Member:对应 Global Actor-isolated;nonisolatedMember:不属于 Actor Executor;- Local Immutable Value:通常属于当前 Task/Region;
- Lock-protected Class:编译器不自动理解所有自定义同步,可能需审计后的 Sendable Conformance。
6.3 Region-based Isolation
现代 Swift 编译器可在部分场景证明一个 Non-Sendable Value 尚未形成并发别名,从而允许安全 Transfer。诊断能力随 Toolchain 演进,不应通过“当前编译器没报错”推导类型可任意跨任务共享;公开 API 仍应优先设计清晰 Sendable Value/Actor Ownership。
七、Sendable:跨隔离传递的类型契约
7.1 Value Type
struct SearchRequest: Sendable {
let query: String
let page: Int
let accountID: UUID
}成员均可安全跨域,编译器可检查合成 Conformance。若包含 Non-Sendable Mutable Class,Conformance 会失败或需要重新设计。
7.2 Reference Type
安全的 Final Immutable Reference:
final class APIEndpoint: Sendable {
let baseURL: URL
let headers: [String: String]
init(baseURL: URL, headers: [String: String]) {
self.baseURL = baseURL
self.headers = headers
}
}Reference Type 也可内部用 Lock 保护可变状态:
final class CancellationFlag: @unchecked Sendable {
private let lock = NSLock()
private var cancelled = false
func cancel() {
lock.withLock { cancelled = true }
}
var isCancelled: Bool {
lock.withLock { cancelled }
}
}这里使用 @unchecked,因为编译器无法验证 Lock Protocol。审计要求:所有访问都在 Lock 内、没有返回内部 Mutable Reference、Callback 不在持锁时调用、Subclass 不可破坏不变量。
7.3 Actor Reference
Actor Reference 可以跨域传递;调用其 Isolated Method 仍需 Await。Actor 内部 Non-Sendable State 没有逃逸时可以安全存在。
7.4 @Sendable Closure
func perform(
operation: @escaping @Sendable (SearchRequest) async throws -> Void
) {
// Store or execute across concurrency domains.
}@Sendable 限制 Capture:
var count = 0
Task.detached {
// Swift 6: concurrently capturing/mutating local var is unsafe.
count += 1
}Weak Capture 只改变 Retention,不使 Mutable Object 自动 Sendable。UIView Weak Reference 仍然只能在 MainActor 上使用。
7.5 Sendable 不保证业务正确
一个 struct TransferRequest: Sendable 可安全复制到多个 Task,但两次请求仍可能重复扣款。Sendable 解决跨域 Memory Safety,不解决 Idempotency、Authorization 或 Transaction。
八、Data Race Safety:Actor 能解决什么
8.1 Data Race 定义
当多个并发执行上下文访问同一内存位置,至少一个是 Write,且缺少有效同步,就可能发生 Data Race。结果包括:
- 读取撕裂/旧值;
- Collection 内部损坏;
- Use-after-free;
- 难以复现的 Crash;
- Undefined Behavior。
Actor、Lock、Atomics、Immutable Value Transfer 都可建立同步/所有权边界。
8.2 Actor 串行化隔离片段
actor Counter {
private var value = 0
func increment() {
value += 1
}
func snapshot() -> Int {
value
}
}多个 Task 并发 await counter.increment() 不会同时修改 value。但调用完成顺序不应被用于业务排序,除非另有 Sequence/ID Contract。
8.3 Data Race 与 Race Condition
Data Race:无同步并发访问同一内存,语言级不安全。
Logical Race:每次访问都隔离正确,但事件顺序造成错误业务结果。Actor 重点解决前者;Reentrancy、重复提交和旧结果覆盖属于后者。
九、Reentrancy:await 把方法切成多个事务片段
9.1 典型余额竞态
actor Wallet {
private var balance: Decimal
private let paymentService: PaymentService
func pay(amount: Decimal) async throws {
guard balance >= amount else { throw PaymentError.insufficientFunds }
try await paymentService.authorize(amount: amount)
balance -= amount
}
}两个 pay 调用都可能在第一个 Await 前看到足够 Balance,然后都去授权,恢复后分别扣款。没有 Data Race,但业务不变量被破坏。
9.2 修复不能只加 Actor
方案取决于业务:
- 预留再 Await:先在 Actor 内扣减/冻结额度,失败后补偿;
- 状态机:记录 Pending Payment ID,禁止冲突操作;
- Revision:Await 后检查状态版本,不匹配则拒绝提交;
- 合并 In-flight Task:相同 Key 共享一次操作;
- Server Transaction/Idempotency:最终一致性由服务端保证;
- 无 Await Critical Fragment:把本地不可分割变更放在同步 Actor Method。
9.3 预留模式
actor Wallet {
private var available: Decimal
private var reservations: [UUID: Decimal] = [:]
func pay(amount: Decimal) async throws {
let paymentID = UUID()
try reserve(amount: amount, paymentID: paymentID)
do {
try await paymentService.authorize(
id: paymentID,
amount: amount
)
commit(paymentID: paymentID)
} catch {
rollback(paymentID: paymentID)
throw error
}
}
private func reserve(amount: Decimal, paymentID: UUID) throws {
guard available >= amount else {
throw PaymentError.insufficientFunds
}
available -= amount
reservations[paymentID] = amount
}
private func commit(paymentID: UUID) {
reservations[paymentID] = nil
}
private func rollback(paymentID: UUID) {
guard let amount = reservations.removeValue(forKey: paymentID) else {
return
}
available += amount
}
}本地不变量改善了,但生产支付仍需 Server Idempotency、超时后的未知状态查询、App Crash 后 Durable Reservation 和补偿重试。内存 Actor 不是金融事务。
9.4 Actor 不承诺公平性/严格 FIFO
Runtime Scheduling、Priority、Suspension 和 Reentrancy 会影响执行顺序。若业务要求 Order,应显式建 Sequence Number、Queue State Machine 或 Durable Log。
十、nonisolated:从隔离域中安全地拿出成员
10.1 适合纯值描述
actor ImageCache: CustomStringConvertible {
nonisolated let name: String
private var entries: [URL: Data] = [:]
init(name: String) {
self.name = name
}
nonisolated var description: String {
"ImageCache(\(name))"
}
}description 不需要 Await,因为只读取不可变、可安全访问的 name。不能在其中读取 entries.count,因为 entries 是 Actor-isolated Mutable State。
10.2 Protocol Conformance
某些同步 Protocol Requirement 不能 Await Actor。可选方案:
- Requirement 只需 Immutable Snapshot:实现为
nonisolated; - Protocol 本身标 Actor/Global Actor;
- 提供 Async Requirement;
- 用 Adapter 缓存 Sendable Snapshot;
- 不让 Actor 直接 Conform。
不要为满足同步 Protocol 把 Mutable State 标成 nonisolated(unsafe)。
10.3 nonisolated(unsafe)
它抑制部分隔离检查,不添加同步。只适合底层代码已由外部 Lock/Runtime Contract 严格保护、且团队能审计的迁移边界。优先把同步封装到独立 Sendable Type,避免在 Actor 内开安全后门。
10.4 Nonisolated Async 的执行语义
Nonisolated Async Function 不拥有该 Actor 的隔离访问权;它的具体 Executor 行为受到 Swift Language Mode/Upcoming Feature 影响。不要用它作为“后台线程”注解,只将其用于不依赖 Actor State、输入输出满足 Sendable 的逻辑。
十一、跨隔离调用:Hop、传值与批量接口
11.1 Actor Hop
let snapshot = await store.snapshot()
await MainActor.run {
viewModel.apply(snapshot)
}每次 Hop 都有调度和 Reentrancy 语义。性能关键循环不要逐元素 Hop:
// 较差:N 次跨 Actor 调用。
for id in ids {
results.append(await store.record(id: id))
}
// 更清晰:一个批量隔离操作。
let results = await store.records(ids: ids)先测量再优化;批量 API 还应控制返回数据量和复制成本。
11.2 传递 Snapshot,而不是暴露内部对象
struct SessionSnapshot: Sendable {
let accountID: UUID
let accessTokenExpiry: Date
let revision: Int
}Snapshot 让调用方读到一个一致版本,但它会过期。操作提交时仍需带 Revision/Token 再验证,不能长期持有 Snapshot 认为 Actor State 不再变化。
11.3 isolated Parameter
func clearExpiredEntries(
in cache: isolated ImageCache,
before date: Date
) {
cache.removeEntries(before: date)
cache.recordMaintenance(date: date)
}isolated Parameter 让整个函数借用某个 Actor 的隔离域,多个调用可在一次 Hop 内完成。一个函数不能同时借用两个不同 Actor 的隔离域;跨两个 Actor 的原子事务仍需重新设计 Ownership。
11.4 避免 Ping-pong
Actor A Await B,B 又频繁 Await A,会产生大量 Hop 和复杂重入。通常说明状态边界切得过细。可以:
- 合并强耦合状态到一个 Owner;
- 传 Sendable Snapshot;
- Parent Orchestrator 协调,避免 Actor 相互回调;
- 用 Batch Command/Result;
- 将纯计算变为 Nonisolated Sendable Function。
十二、Objective-C 回调桥接
12.1 问题:Objective-C 不认识 Swift Isolation
旧 Delegate/SDK 可能:
- 未标 Callback Queue;
- 可从任意 Thread 回调;
- 回调多次或重入;
- 传入 Non-Sendable Mutable Object;
- 取消后仍晚到;
- Protocol Requirement 是同步 Nonisolated。
不能仅给 Delegate 类型加 @MainActor 就假设 Runtime 自动切到 Main Thread。
12.2 Nonisolated Adapter → MainActor
@MainActor
final class DownloadViewModel: NSObject {
private var revision = 0
private(set) var progress: Double = 0
nonisolated func downloader(
_ downloader: LegacyDownloader,
didUpdateProgress progress: Double,
revision callbackRevision: Int
) {
let normalized = min(max(progress, 0), 1)
Task { @MainActor [weak self] in
guard let self else { return }
guard revision == callbackRevision else { return }
self.progress = normalized
}
}
}Adapter 在任意线程可进入,只对 Sendable Value 做纯归一化,然后创建 MainActor Task。它改变了同步时序:Delegate 返回时 UI 可能尚未更新。若回调顺序关键,应通过单一 Adapter Actor/Sequence Number 串行处理,而不是假设多个 Task 的调度顺序。
12.3 Non-Sendable 参数
若 Callback 传 NSMutableData、UIKit Object 或 SDK Mutable Model,不应直接捕获进 @Sendable Task。可选:
- 在 Callback Contract 允许的线程内复制成
Data/Sendable DTO; - 让 SDK 固定回调到 Main Queue,再在 MainActor 窄边界验证;
- 写 Lock-protected Adapter,内部生成 Value Snapshot;
- 向 SDK 请求原生 Async/Sendable API。
复制也要考虑数据量和性能,不能把数百 MB Buffer 每个 Progress Callback 都复制。
12.4 MainActor.assumeIsolated
nonisolated func legacyCallbackGuaranteedOnMainThread() {
MainActor.assumeIsolated {
applyLegacyUpdate()
}
}只有官方/自有 API Contract 明确保证当前就在 MainActor-compatible Main Execution Context 时才使用,并在测试中验证。未知线程 Callback 应异步 Hop,而不是 Assume。
12.5 @preconcurrency Conformance/Import
它用于迁移尚未具备现代并发标注的模块或协议,降低/延迟部分诊断责任,但 Runtime 行为不变。使用时应:
- 建 Adapter Boundary;
- 记录 Callback Queue/Ownership;
- 加 Thread/Isolation Assertion 和 Stress Test;
- 跟踪上游 SDK 更新;
- 不把整个 App 的 Import 都永久标注后遗忘。
十三、编译语言模式差异
13.1 三个相互独立的维度
| 维度 | 影响 |
|---|---|
| Swift Language Mode | 哪些语言规则成为 Error/默认语义 |
| Strict Concurrency Checking | Minimal/Targeted/Complete 的诊断覆盖 |
| Default Actor Isolation | 未显式标注声明默认落在哪个隔离域 |
同一 Toolchain 中,Target A/B 的设置不同就可能得到不同诊断。Code Review 必须知道文件属于哪个 Module/Target。
13.2 Swift 5 Mode
开启 Strict Concurrency 后可逐步得到 Sendable/Actor Warning,但某些潜在问题仍只警告或受兼容规则放宽。代码“编译通过”不代表能安全迁移到 Swift 6。
13.3 Swift 6 Mode
Swift 6 将 Complete Concurrency Checking 下的许多潜在 Data Race 提升为 Error,并更严格检查:
- Non-Sendable Value 跨隔离;
@SendableClosure Capture;- Actor-isolated Protocol Conformance;
- Global/Static Mutable State;
- MainActor Call;
deinit/Closure/Task Transfer 等生命周期边界。
具体诊断随 Swift 6.x 改进,不能把某个版本的错误文本写进 Architecture Contract。
13.4 Default Actor Isolation
现代 Xcode/Swift 支持为 Module 选择 Default Isolation(例如 MainActor 或 Nonisolated,具体选项/名称以当前 Toolchain 为准)。App/UI Module 选择 MainActor Default 可减少样板,但也会:
- 让未显式标注的类型/函数成为 MainActor-isolated;
- 影响 Protocol Conformance 和 Test;
- 让纯计算意外需要 Hop;
- 与 Package/Framework 的 Nonisolated Default 产生边界诊断。
Library 通常更需要显式、最小化 Isolation,避免把调用方强制到 MainActor。
13.5 迁移策略
不要从批量添加 @unchecked Sendable 开始。先修正 Ownership,再处理 Compiler Diagnostics。
十四、工程案例:会话状态、缓存与 UI 的隔离边界
14.1 Domain Model
struct SessionSnapshot: Sendable, Equatable {
let accountID: UUID
let displayName: String
let revision: Int
}
actor SessionStore {
private var snapshot: SessionSnapshot?
private var revision = 0
func current() -> SessionSnapshot? {
snapshot
}
func replace(accountID: UUID, displayName: String) -> SessionSnapshot {
revision += 1
let next = SessionSnapshot(
accountID: accountID,
displayName: displayName,
revision: revision
)
snapshot = next
return next
}
func clear() {
revision += 1
snapshot = nil
}
func isCurrent(_ candidate: SessionSnapshot) -> Bool {
snapshot == candidate
}
}Actor 防止 Session 内存状态并发破坏,Snapshot 可安全跨域传递。
14.2 MainActor UI Model
@MainActor
final class AccountViewModel {
enum State {
case signedOut
case loading
case signedIn(SessionSnapshot)
case failed(String)
}
private let sessionStore: SessionStore
private let profileService: ProfileService
private var loadTask: Task<Void, Never>?
private(set) var state: State = .signedOut
func load(accountID: UUID) {
loadTask?.cancel()
state = .loading
loadTask = Task { [weak self] in
guard let self else { return }
do {
let profile = try await profileService.profile(id: accountID)
try Task.checkCancellation()
let snapshot = await sessionStore.replace(
accountID: accountID,
displayName: profile.displayName
)
try Task.checkCancellation()
guard await sessionStore.isCurrent(snapshot) else { return }
state = .signedIn(snapshot)
} catch is CancellationError {
return
} catch {
state = .failed(error.localizedDescription)
}
}
}
func signOut() {
loadTask?.cancel()
loadTask = Task { [weak self] in
guard let self else { return }
await sessionStore.clear()
state = .signedOut
}
}
deinit {
loadTask?.cancel()
}
}14.3 案例中的重入问题
若 load(accountA) Await 网络期间调用 signOut(),仅取消未必能终止不可取消请求;Task.checkCancellation() 和 Session Current Check 一起防止 A 重新覆盖 Signed-out UI。
但上面代码仍需更严格产品策略:
signOut与load是否共享同一个 Operation Revision;- Account B 登录后,A 是否可能先写 SessionStore 再被 Check 拒绝;
- Token/Keychain 清理是否 Durable;
- Server Session Revoke 失败如何处理;
- App 重启如何恢复。
更稳妥可让 SessionStore 持有 Operation Revision/Expected Account,把“仅当当前操作仍匹配时 Replace”做成一个无 Await Isolated Commit:
actor SessionStore {
private var operationRevision = 0
private var snapshot: SessionSnapshot?
func beginOperation() -> Int {
operationRevision += 1
return operationRevision
}
func commit(
_ candidate: SessionSnapshot,
operation: Int
) -> Bool {
guard operation == operationRevision else { return false }
snapshot = candidate
return true
}
func invalidateOperationsAndClear() {
operationRevision += 1
snapshot = nil
}
}这把 Check-and-Set 留在同一个无 Await Actor Fragment 内,消除“外部先 Check 后 Set”的窗口。真正登录态还需 Keychain/Server 的一致性协议。
14.4 隔离矩阵
| 数据/行为 | 隔离域 | 跨域形式 | 防重入策略 |
|---|---|---|---|
| UI State | MainActor | MainActor Method/Task | Task Handle + Revision |
| Session Snapshot | SessionStore Actor | Sendable Value | Operation Revision |
| Database Record | DatabaseActor | Batch Method/DTO | DB Transaction |
| Profile Network | Nonisolated Sendable Service | Async Result | Cancellation + Request ID |
| Legacy Callback | Nonisolated Adapter | Copy DTO + Actor Hop | Sequence/Revision |
十五、常见误区与修复
15.1 错误:Actor 就是一条固定线程
问题: 依赖 Thread-local 或 Thread ID。
修复: Actor 是隔离/Executor 抽象,Runtime 可用不同线程执行不同片段;依赖 Actor Contract。
15.2 错误:Actor Async Method 从头到尾原子
问题: Await 前检查、Await 后提交,期间状态已被其他任务改变。
修复: Await 后重验 Revision/Invariant,或使用预留、状态机和无 Await Commit Fragment。
15.3 错误:MainActor 会把同步 CPU 重任务自动移到后台
问题: 大循环/解码仍阻塞 UI。
修复: MainActor 只隔离主执行状态;CPU Work 使用 Sendable Input 在合适 Nonisolated Service/Task 中执行。
15.4 错误:Task { @MainActor in } 等于 MainActor.run
问题: 前者新建 Unstructured Task 且调用方不等待,改变 Error/Cancel/Order。
修复: 需要顺序结果时 await MainActor.run 或直接声明 MainActor API;Callback Fire-and-hop 才谨慎用 Task。
15.5 错误:Sendable 等于 Deeply Immutable
问题: 忽略内部 Lock/Actor 或引用成员。
修复: Sendable 表示跨域传递安全;具体通过 Value Semantics、Immutable Final Class、内部同步或 Actor 达成。
15.6 错误:Weak Capture 使 Non-Sendable 对象可安全跨域
问题: Weak 只解决 Retention,不解决并发访问。
修复: UIView 等仍在 MainActor 使用;跨域传 Sendable Snapshot/ID。
15.7 错误:给所有类型加 @unchecked Sendable
问题: 只是关闭编译器保护,真实 Data Race 仍存在。
修复: 先用 Value/Actor/Lock 重建 Ownership;仅对已审计同步封装使用 Unchecked。
15.8 错误:nonisolated 是性能优化注解
问题: 为减少 Await 暴露 Mutable State。
修复: 只用于不依赖隔离状态的纯逻辑/Immutable Member;批量 API 才是减少 Hop 的常规方法。
15.9 错误:Objective-C Callback 标 MainActor 后 Runtime 自动跳主线程
问题: Objective-C 不执行 Swift Actor Hop,旧协议 Annotation 也可能缺失。
修复: 按文档验证 Queue,在 Nonisolated Adapter 复制 Sendable Input,再显式 Hop;未知线程不使用 AssumeIsolated。
15.10 错误:Swift 5 Mode 没报错说明代码安全
问题: 兼容模式可能只有 Warning/放宽检查。
修复: 开启 Complete Strict Checking,逐模块迁移 Swift 6,并用压力测试验证 Logical Race。
15.11 错误:Actor 能替代服务端/数据库事务
问题: Actor 只保证单进程内隔离片段。
修复: Durable/跨设备一致性仍依赖数据库 Transaction、唯一约束、Idempotency 和 Server State Machine。
十六、测试与验证
16.1 编译期验证
建议迁移阶段使用:
- Swift 6 Language Mode(分模块推进);
- Complete Strict Concurrency;
- Warning 视为 CI Failure(在迁移豁免列表外);
- 明确 Default Actor Isolation;
- 审计每个
@unchecked Sendable、nonisolated(unsafe)和@preconcurrency; - Public API 的 Sendable/Actor Annotation 做 Source Compatibility Review。
16.2 重入测试
用 Controllable Suspension Point 构造确定顺序:
- Operation A 读取状态并暂停;
- Operation B 改变状态/注销/切账号;
- 恢复 A;
- 断言 A 被 Revision/Invariant 拒绝;
- 断言无旧 UI/缓存提交;
- 覆盖 Success、Error、Cancel 和 Timeout。
不要只用随机 Sleep 猜竞态。
16.3 Objective-C Bridge 测试
- 从 Main/Background/Concurrent Queue 回调;
- 同步重入 Callback;
- 连续多次/乱序 Callback;
- Cancel 后晚到;
- Non-Sendable 参数复制边界;
- Owner Deinit 后 Callback;
assumeIsolated边界加 Executor/Thread Precondition(按 API Contract)。
16.4 工具
| 工具 | 能发现 | 不能证明 |
|---|---|---|
| Swift 6 Compiler | 许多跨隔离/Sendable 问题 | 业务 Logical Race 全部正确 |
| Thread Sanitizer | 部分运行时 Data Race | 未覆盖路径无 Race |
| Swift Concurrency Instrument | Task/Actor Hop/Wait/Continuation | 服务端一致性 |
| Time Profiler | Actor 上长同步工作 | 所有时序不变量 |
| System Trace | Thread/Executor/Lock Scheduling | Actor 业务状态含义 |
| Memory Graph | Task/Closure/Adapter Retain Cycle | 取消协议正确 |
16.5 性能验证
Actor Hop 通常不是第一优化目标。先测量:
- MainActor Long Synchronous Fragment;
- Actor Queue/Executor 等待;
- A↔B Ping-pong Hop 次数;
- 批量接口前后 P95/P99;
- Snapshot Copy/Allocation;
- Lock-based Sendable Wrapper Contention;
- Task/Callback Burst;
- UI Hitch 与能耗。
不要为了减少 Hop 把隔离移除,除非有数据证明并有等价安全方案。
十七、方案选择清单
| 问题 | 优先方案 | 警惕 |
|---|---|---|
| 独立可变业务状态 | Actor Instance | 跨 Await 不变量 |
| UI/ViewModel 状态 | MainActor | 同步重 CPU Work |
| 多类型共享串行 SDK/DB | Custom Global Actor | 巨大全局瓶颈 |
| 跨域 DTO | Sendable Value | Snapshot 过期 |
| 线程安全 Mutable Class | Actor 或审计后的 Lock Wrapper | 滥用 unchecked |
| 同步协议描述 | nonisolated Immutable Member | 读取隔离 Mutable State |
| 多次跨 Actor 操作 | Batch API / isolated Parameter | Ping-pong、双 Actor 事务 |
| Objective-C 任意线程回调 | Nonisolated Adapter + Explicit Hop | AssumeIsolated、乱序 |
| 迁移旧 SDK | 窄范围 @preconcurrency Adapter | 永久关闭诊断 |
十八、总结
Actor 隔离把“谁能访问状态”提升为类型系统契约。Actor Instance、MainActor 和 Custom Global Actor 分别适合独立状态、UI 主执行域和跨类型共享系统边界;跨域调用通过 Await 和 Sendable Value 建立安全通道。Actor 不是线程,也不是数据库事务。
Sendable 描述跨域传递安全,@Sendable 约束 Closure Capture。Value Semantics、Immutable Reference、Lock Wrapper 和 Actor 都可以形成安全实现;@unchecked Sendable 只是一份人工承诺,绝不是修复动作。
Actor 的最大工程边界是 Reentrancy。Await 会把方法分成多个隔离片段,期间其他任务可以改变状态。检查后执行、余额预扣、Token 刷新、页面加载都必须用 Revision、Reservation、In-flight Task 或无 Await Commit Fragment 维护业务不变量。
Objective-C 和旧 SDK 是隔离最薄弱的边界。Callback Queue、可变参数、次数和取消必须先验证,再通过 Nonisolated Adapter 复制成 Sendable DTO 并显式 Hop。assumeIsolated 与 @preconcurrency 只能用于已证明的窄边界。
最后,Swift 6 编译通过只证明更多 Data Race 被排除,不证明业务顺序正确。真正可靠的并发系统需要严格编译设置、可控重入测试、Bridge 压力测试和 Instruments 证据共同守护。
问答复盘
Q1:Actor 是否等于一条 Serial Queue 或固定线程?
答: 不等于。Actor 是语言级隔离域,由 Executor 调度;不同隔离片段可能运行在不同线程,代码不能依赖 Thread Identity。
Q2:为什么跨 Actor 调用同步声明的方法仍需要 await?
答: 调用方必须等待目标 Actor 获得执行机会。方法内部虽无 Suspension,跨隔离 Hop 本身是异步等待边界。
Q3:Actor 能否保证一个 async 方法从入口到返回原子执行?
答: 不能。每个 await 都可能允许其他任务进入;Actor 只串行化隔离同步片段,恢复后必须重验状态和不变量。
Q4:MainActor.run 与 Task { @MainActor in } 有什么区别?
答: MainActor.run 会 Await MainActor Closure 完成;Task 创建新的 Unstructured Task,调用方不自动等待,Error、Cancel 和 Order 语义不同。
Q5:两个同类型 Actor Instance 是否共享一个隔离队列?
答: 不共享。每个 Actor Instance 是独立隔离域,可并发执行;需要跨类型/实例共享域时使用同一 Actor Instance 或 Custom Global Actor。
Q6:Sendable 是否表示对象绝对不可变?
答: 不表示。它表示跨并发域传递安全;可通过不可变 Value/Reference、内部 Lock 或 Actor Isolation 实现,但业务一致性仍需另行设计。
Q7:weak 捕获能否让 UIView 安全进入 Detached Task?
答: 不能。Weak 只改变引用生命周期,UIView 仍属于 MainActor/UI 隔离;应跨域传 Sendable ID/DTO,并回 MainActor 使用 View。
Q8:nonisolated 最适合什么场景?
答: 不依赖 Actor Mutable State 的同步协议要求、不可变标识和纯计算。它不是绕过 Await 或提高性能的通用开关。
Q9:Objective-C Delegate 文档保证主线程回调时能否使用 MainActor.assumeIsolated?
答: 只可在契约明确且测试验证的窄 Adapter 使用;若线程未知或可能变化,应显式异步 Hop,错误 Assume 可能导致 Runtime Failure。
Q10:Swift 6 Complete Checking 通过后为何仍要测试竞态?
答: 编译器主要证明隔离和 Data Race Safety,无法证明 Await 前后业务状态仍有效。旧结果覆盖、重复支付和账号切换属于 Logical Race,必须用状态机和可控重入测试验证。
延伸知识
- Swift Concurrency Runtime:Executor、Job、Task Scheduling 与 Priority Escalation;
- Region-based Isolation:
sending、Transfer Analysis 与 Diagnostic 演进; - Swift 6.2 Migration:Default Actor Isolation 与 Approachable Concurrency 设置;
- Atomics 与 Lock:Memory Ordering、Critical Section 和 Sendable Wrapper;
- Distributed Actor:远程边界、序列化与失败模型;
- 数据库事务:Actor Isolation 与 ACID/跨进程一致性的区别;
- 并发工程实践:请求取消、Token 刷新合并和 Continuation 状态机;
- Objective-C Interop:Delegate Queue、
@preconcurrency与 SDK Annotation。