说实话,如果你从 Swift 5 一路用过来,对并发的感受大概率就是两个字——头疼。async/await 本身还好,语法清晰、上手也快。可一旦你鼓起勇气开启 Swift 6 的严格并发检查,编译器就像突然翻脸了一样:满屏的 Sendable 错误、Actor 隔离冲突、莫名其妙的"跨隔离域"警告……我见过不少团队卡在这一步,迟迟不敢真正切到 Swift 6。
Apple 显然也意识到了这个问题。
于是在 Swift 6.2(随 Xcode 26 一起发布)中,他们推出了一套叫做 Approachable Concurrency 的改进方案。核心思路其实很简单:让并发变成你主动选择的东西,而不是编译器强塞给你的。我个人觉得这个方向终于对了。
这篇文章会把 Swift 6.2 的三个核心并发变化讲透——MainActor 默认隔离、nonisolated(nonsending) 默认行为、以及新的 @concurrent 注解。每个概念都配了可运行的代码示例,看完你就能在自己项目里放心启用这些特性了。
为什么需要 Approachable Concurrency?
先回顾一下痛点吧。在 Swift 6.0/6.1 中,如果你开启了严格并发检查,下面这种日常场景就能让你抓狂:
// Swift 6.0 — 这段代码会报错
class UserManager {
var currentUser: User? // ⚠️ 全局变量不是并发安全的
func fetchUser() async -> User {
let data = try await URLSession.shared.data(from: url)
let user = try JSONDecoder().decode(User.self, from: data.0)
currentUser = user // ⚠️ 从非隔离的异步上下文修改属性
return user
}
}
就这么一个简单的网络请求,编译器能给你抛出一堆错误:currentUser 不是 Sendable 的、异步函数运行在全局执行器上导致跨隔离域访问……你明明只是想发个网络请求而已,却被迫先上一堂并发理论课。
Approachable Concurrency 的设计哲学是渐进式披露(Progressive Disclosure),说白了就是:
- 先写顺序代码——默认一切运行在 MainActor 上,不需要任何并发注解
- 需要异步时加 async/await——但函数仍然留在调用者的隔离域内
- 真正需要并行时才引入并发——用
@concurrent明确标记需要后台执行的函数
这意味着大多数 iOS 开发者在日常开发中根本不需要接触复杂的并发概念。只有当你确实需要把重活丢到后台(比如图片处理、大批量 JSON 解析)的时候,才需要显式地"选择进入"并发世界。这个思路,对吧?
核心变化一:MainActor 默认隔离(SE-0466)
这是 Swift 6.2 最大的一个改变,也是我觉得最实用的一个:你的代码默认运行在 MainActor 上,除非你明确说不需要。
之前 vs 现在
在 Swift 6.0/6.1 中,代码默认是 nonisolated 的——也就是没有隔离到任何 Actor。这导致你需要手动给大量 UI 相关代码加上 @MainActor:
// Swift 6.0 — 需要到处标注 @MainActor
@MainActor
class ProfileViewModel: ObservableObject {
@MainActor @Published var userName: String = ""
@MainActor @Published var isLoading: Bool = false
@MainActor
func loadProfile() async {
isLoading = true
// ...
}
}
看着就累,是不是?
在 Swift 6.2 开启 MainActor 默认隔离后:
// Swift 6.2 — 默认就在 MainActor 上,干净多了
class ProfileViewModel: ObservableObject {
@Published var userName: String = ""
@Published var isLoading: Bool = false
func loadProfile() async {
isLoading = true
// ...
}
}
没有任何 @MainActor 注解,但编译器知道一切都运行在主线程上。这跟大多数 iOS 应用的实际运行方式完全一致——你的 ViewModel、Service、Manager 大部分时间本来就应该待在主线程上。
需要退出 MainActor 隔离时怎么办?
有些代码确实不需要绑在 MainActor 上,这时候用 nonisolated 标记就行:
// 模型层通常不需要绑定到任何 Actor
nonisolated struct UserProfile: Codable, Sendable {
let id: UUID
let name: String
let email: String
}
// 纯计算函数也不需要
nonisolated func calculateHash(for data: Data) -> String {
data.base64EncodedString()
}
思路特别清晰:默认安全(MainActor),按需退出(nonisolated)。比以前那套"默认裸奔(nonisolated),到处加锁(@MainActor)"好太多了。
核心变化二:nonisolated(nonsending) 默认行为(SE-0461)
这个变化解决了一个让很多人困惑已久的问题:nonisolated 异步函数到底运行在哪里?
Swift 6.0 的混乱行为
在 Swift 6.0 中,nonisolated 的同步函数和异步函数的行为是不一致的,这一点坑了不少人:
class DataProcessor {
// 同步函数:运行在调用者的线程上(符合直觉)
nonisolated func processSync() {
// 如果从 MainActor 调用,就在主线程运行
}
// 异步函数:跳到全局执行器上!(完全不符合直觉)
nonisolated func processAsync() async {
// 不管从哪里调用,都会跑到后台线程
// 这意味着你传入的参数必须是 Sendable 的
}
}
异步函数会自动跳到全局后台执行器——这等于偷偷创建了一个新的隔离域。于是编译器要求所有传入参数必须是 Sendable 的,即使你根本没打算做任何并行操作。说实话,这个设计决策当初让我困惑了很久。
Swift 6.2 的统一行为
现在好了。nonisolated 异步函数的默认行为变成了 nonisolated(nonsending)——继承调用者的隔离上下文:
// Swift 6.2 — nonisolated 异步函数默认继承调用者隔离
class DataProcessor {
nonisolated func processAsync() async {
// 从 MainActor 调用?就在 MainActor 上运行
// 从某个自定义 Actor 调用?就在那个 Actor 上运行
// 不再自动跳到后台线程
}
}
// 实际使用
@MainActor
class ViewModel {
let processor = DataProcessor()
func doWork() async {
// processAsync() 会在 MainActor 上运行
// 不需要 Sendable,不会跨隔离域
await processor.processAsync()
}
}
好处一目了然:
- 不再需要到处标 Sendable——因为根本没有跨隔离域
- 行为与同步函数一致——少了一个需要记住的特殊规则
- 代码更容易推理——你永远知道函数在哪里运行
显式标记 nonisolated(nonsending)
如果你的项目暂时还没启用 NonisolatedNonsendingByDefault 特性标志,也可以先对单个函数手动标记:
class NetworkService {
// 手动标记:继承调用者的隔离上下文
nonisolated(nonsending) func fetchData(from url: URL) async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
}
(这在渐进式迁移的时候特别有用。)
核心变化三:@concurrent 注解
OK,既然 nonisolated 异步函数不再自动跑到后台线程了,那当你真的需要在后台执行耗时操作时怎么办?
这就是 @concurrent 登场的时候了。
什么时候用 @concurrent
@concurrent 明确告诉编译器:"这个函数应该在全局并发执行器上运行,别留在调用者的 Actor 上。"
class ImageProcessor {
// 这个函数做 CPU 密集型工作,应该在后台线程运行
@concurrent
func applyFilters(to image: UIImage) async -> UIImage {
// 复杂的图片处理——不应该阻塞主线程
let ciImage = CIImage(image: image)!
let filter = CIFilter(name: "CIGaussianBlur")!
filter.setValue(ciImage, forKey: kCIInputImageKey)
filter.setValue(10.0, forKey: kCIInputRadiusKey)
// ... 更多滤镜处理
return processedImage
}
// 这个函数只是做网络请求,不需要后台线程
// 默认就是 nonisolated(nonsending),继承调用者隔离
nonisolated func fetchImageMetadata(id: String) async throws -> ImageMetadata {
let url = URL(string: "https://api.example.com/images/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(ImageMetadata.self, from: data)
}
}
@concurrent vs nonisolated(nonsending) 决策指南
到底什么时候用哪个?其实判断标准很直接:
// ✅ 用 nonisolated(默认 nonsending):
// - 网络请求(大部分时间在等 I/O,不占 CPU)
// - 简单的数据转换
// - 不涉及大量计算的操作
nonisolated func fetchUser() async throws -> User { ... }
// ✅ 用 @concurrent:
// - 图片/视频处理
// - 大量 JSON 解析(比如解析几 MB 的数据)
// - 加密/哈希计算
// - 任何会阻塞线程超过几毫秒的 CPU 工作
@concurrent
func processLargeDataset(_ data: Data) async -> [Record] { ... }
记住一条经验法则就够了:如果操作主要是等待(I/O bound),用默认的 nonsending;如果操作主要是计算(CPU bound),用 @concurrent。
推断隔离一致性(SE-0470)
Swift 6.2 还顺带解决了一个让人特别烦的小问题:推断隔离一致性(Inferred Isolated Conformances)。
之前如果你有一个 @MainActor 类型想遵循 Equatable,你得写一堆别扭的 nonisolated 变通代码:
// Swift 6.0 — 痛苦的变通方案
@MainActor
class UserSettings {
var theme: String = "light"
var fontSize: Int = 14
// 必须标记为 nonisolated 才能遵循 Equatable
// 但这意味着从非主线程也能调用,破坏了隔离保证
nonisolated static func == (lhs: UserSettings, rhs: UserSettings) -> Bool {
// ⚠️ 这里访问 actor 隔离的属性其实不安全
lhs.theme == rhs.theme && lhs.fontSize == rhs.fontSize
}
}
说白了就是为了让编译器闭嘴,你不得不写出不安全的代码。这算什么事?
Swift 6.2 中,编译器终于聪明了——既然类型是 @MainActor 的,那它的协议遵循自然也应该在 MainActor 上下文中:
// Swift 6.2 — 编译器自动推断隔离一致性
@MainActor
class UserSettings: Equatable {
var theme: String = "light"
var fontSize: Int = 14
static func == (lhs: UserSettings, rhs: UserSettings) -> Bool {
// ✅ 编译器知道这个 conformance 仅在 MainActor 上有效
lhs.theme == rhs.theme && lhs.fontSize == rhs.fontSize
}
}
这个改进对 Hashable、Comparable、CustomStringConvertible 等常见协议都适用。终于不用为了遵循个协议就写不安全的代码了。
如何启用 Approachable Concurrency
好了,概念都讲完了,来看看具体怎么在项目里开启。
Xcode 26 新项目
如果你用 Xcode 26 创建新项目,恭喜——Approachable Concurrency 默认就是开启的。Build Settings 中会自动设置好:
- Approachable Concurrency:Yes
- Default Actor Isolation:MainActor
开箱即用,不需要做任何额外配置。
已有项目迁移
对于已有项目,你需要手动开启。在 Xcode 的 Build Settings 里操作:
- 搜索 "Approachable Concurrency",设为
Yes(对应编译器标志SWIFT_APPROACHABLE_CONCURRENCY) - 搜索 "Default Actor Isolation",设为
MainActor
这两步会同时启用以下 Swift 特性标志:
NonisolatedNonsendingByDefault— nonisolated 异步函数默认继承调用者隔离InferIsolatedConformances— 自动推断协议一致性的隔离GlobalActorIsolatedTypesUsability— 改善全局 Actor 类型的使用体验
Swift Package 中启用
如果你在写 Swift Package,在 Package.swift 中配置 swiftSettings:
// Package.swift
let package = Package(
name: "MyLibrary",
platforms: [.iOS(.v26)],
targets: [
.target(
name: "MyLibrary",
swiftSettings: [
.defaultIsolation(MainActor.self),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableUpcomingFeature("InferIsolatedConformances"),
]
),
]
)
其中 .defaultIsolation(MainActor.self) 是 Swift 6.2 新增的 Package Manifest API,语义很直观。
已有项目迁移实战
启用这些特性之后,你的已有代码可能会有行为变化。下面这几个是最容易踩的坑。
1. nonisolated 异步函数不再跑到后台了
这是迁移过程中最容易出问题的地方,务必注意。以前依赖 nonisolated 异步函数自动在后台执行的代码,现在会留在调用者的 Actor 上:
class HeavyLifter {
// 在 Swift 6.0 中,这个函数自动在后台线程运行
// 在 Swift 6.2 中(启用 NonisolatedNonsendingByDefault),
// 它会留在调用者的线程上!
nonisolated func crunchNumbers() async -> [Double] {
// 如果从 MainActor 调用,这段代码会阻塞主线程!
return (0..<1_000_000).map { Double($0) * Double($0) }
}
}
修复很简单——给需要在后台运行的函数加上 @concurrent:
class HeavyLifter {
@concurrent
func crunchNumbers() async -> [Double] {
// ✅ 现在明确在后台线程运行
return (0..<1_000_000).map { Double($0) * Double($0) }
}
}
2. 全局变量默认隔离到 MainActor
启用默认 MainActor 隔离后,全局变量和静态属性会自动归属 MainActor。如果你从后台线程访问它们,编译器会报错:
// 这个全局变量现在默认隔离到 MainActor
var globalCache: [String: Data] = [:]
@concurrent
func backgroundTask() async {
// ⚠️ 错误:不能从非 MainActor 上下文访问 MainActor 隔离的属性
globalCache["key"] = someData
}
// 修复方案一:标记为 nonisolated
nonisolated var globalCache: [String: Data] = [:]
// 修复方案二:用 Actor 封装(更推荐)
actor CacheManager {
var cache: [String: Data] = [:]
func set(_ key: String, data: Data) {
cache[key] = data
}
}
3. 迁移检查清单
建议按这个顺序来,别着急一步到位:
- 先在一个模块上试点——千万不要一次性全量启用
- 审查所有 nonisolated async 函数——确认哪些需要加
@concurrent - 检查全局变量和静态属性——决定它们应该隔离到 MainActor 还是标记为
nonisolated - 跑一遍完整测试——特别关注跟线程相关的行为变化
- 做性能测试——确保没有把本应在后台跑的重活塞到了主线程
实战示例:一个完整的 MVVM 应用
光讲概念总是抽象的。来看一个完整的例子,展示在 Swift 6.2 Approachable Concurrency 下怎么组织代码结构:
import SwiftUI
// ========================================
// 模型层 — 标记为 nonisolated,不绑定任何 Actor
// ========================================
nonisolated struct Article: Codable, Identifiable, Sendable {
let id: UUID
let title: String
let body: String
let publishedAt: Date
}
// ========================================
// 网络层 — 有些方法需要后台执行
// ========================================
class ArticleService {
private let baseURL = URL(string: "https://api.example.com")!
// 网络请求:I/O bound,用默认的 nonsending 就好
// 从 MainActor 调用时会在 MainActor 上运行
// 但 await URLSession 时会挂起,不会阻塞主线程
nonisolated func fetchArticles() async throws -> [Article] {
let url = baseURL.appendingPathComponent("articles")
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Article].self, from: data)
}
// 大数据解析:CPU bound,应该在后台执行
@concurrent
func parseHugeDataset(_ data: Data) async throws -> [Article] {
// 这可能需要几百毫秒,不应阻塞主线程
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode([Article].self, from: data)
}
}
// ========================================
// ViewModel — 默认在 MainActor 上,完美适配 SwiftUI
// ========================================
@Observable
class ArticleListViewModel {
// 这些属性默认就在 MainActor 上,SwiftUI 可以安全地绑定
var articles: [Article] = []
var isLoading = false
var errorMessage: String?
private let service = ArticleService()
func loadArticles() async {
isLoading = true
errorMessage = nil
do {
// fetchArticles 是 nonisolated(nonsending),
// 继承 MainActor 隔离——但 await 时会挂起,不阻塞 UI
articles = try await service.fetchArticles()
} catch {
errorMessage = "加载失败:\(error.localizedDescription)"
}
isLoading = false
}
}
// ========================================
// View — 正常写 SwiftUI,没有任何变化
// ========================================
struct ArticleListView: View {
@State private var viewModel = ArticleListViewModel()
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading {
ProgressView("加载中...")
} else if let error = viewModel.errorMessage {
ContentUnavailableView(
"出错了",
systemImage: "exclamationmark.triangle",
description: Text(error)
)
} else {
List(viewModel.articles) { article in
VStack(alignment: .leading, spacing: 4) {
Text(article.title)
.font(.headline)
Text(article.body)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(2)
}
}
}
}
.navigationTitle("文章列表")
.task {
await viewModel.loadArticles()
}
}
}
}
注意看这段代码有多干净——没有一个 @MainActor 注解出现,但 ViewModel 的属性更新完全是线程安全的。唯一需要你做的并发决策,就是给 CPU 密集型的 parseHugeDataset 加了个 @concurrent。
这才是并发编程该有的样子嘛。
常见编译器错误与解决方案
启用 Approachable Concurrency 后,你大概率会遇到一些新的编译器错误。别慌,基本上都有固定的解法。
错误 1:跨 Actor 隔离域访问属性
// 错误信息:Main actor-isolated property cannot be referenced
// from a @concurrent function
var cachedData: Data?
@concurrent
func processInBackground() async {
let data = cachedData // ❌ 不能从后台函数访问 MainActor 属性
}
解决方案:先在 MainActor 上获取值,再传给后台函数:
func startProcessing() async {
let data = cachedData // ✅ 在 MainActor 上安全访问
await processInBackground(data: data)
}
@concurrent
func processInBackground(data: Data?) async {
guard let data else { return }
// 处理数据...
}
错误 2:nonisolated 异步函数中修改 Actor 状态
actor DatabaseManager {
var records: [Record] = []
// 这个函数是 nonisolated(nonsending)
// 但它试图修改 actor 的内部状态
nonisolated func importRecords(_ newRecords: [Record]) async {
records.append(contentsOf: newRecords) // ❌ 不能从非隔离上下文修改
}
}
解决方案:去掉 nonisolated,让方法保持在 Actor 隔离内:
actor DatabaseManager {
var records: [Record] = []
// ✅ 作为 actor 的隔离方法,可以安全修改内部状态
func importRecords(_ newRecords: [Record]) {
records.append(contentsOf: newRecords)
}
}
错误 3:第三方库的 Sendable 问题
这个是迁移过程中比较头疼的——有些第三方库还没有跟进 Swift 6.2 的适配,它们的类型可能不是 Sendable 的。当你把这些类型传入 @concurrent 函数时就会报错:
// 第三方库的类型
// class ThirdPartyResult { ... } // 非 Sendable
@concurrent
func processResult(_ result: ThirdPartyResult) async {
// ❌ ThirdPartyResult 不是 Sendable 的
}
临时解决方案:用 @preconcurrency import 先抑制警告,或者干脆改用默认的 nonisolated(nonsending)来绕过跨隔离域的限制:
@preconcurrency import ThirdPartyLib
// 或者:既然不真正需要后台执行,就用默认的 nonsending
nonisolated func processResult(_ result: ThirdPartyResult) async {
// ✅ 不跨隔离域,不需要 Sendable
}
(等库作者适配之后记得回来改掉就好。)
性能注意事项
Approachable Concurrency 确实让代码写起来舒服多了,但有一个潜在陷阱你一定要注意:别无意间把 CPU 密集型工作丢到了主线程上。
在 Swift 6.0 中,nonisolated 异步函数会自动跑后台,所以即使你不小心在里面做了重活,至少不会卡 UI。但 Swift 6.2 改了默认行为——这些函数现在继承调用者隔离,如果调用者在 MainActor 上,你的"重活"就直接在主线程跑了。
开发阶段可以用这个简单的检测方法:
nonisolated func suspiciouslySlowFunction() async {
// 在调试时加上这行来检查当前线程
#if DEBUG
if Thread.isMainThread {
print("⚠️ \(#function) 正在主线程运行,考虑加 @concurrent")
}
#endif
// 你的代码...
}
更靠谱的做法是上 Instruments 的 Time Profiler,看主线程的占用情况。发现某个异步函数在主线程上耗时过长?给它加个 @concurrent 就搞定了。
常见问题(FAQ)
Swift 6.2 Approachable Concurrency 会破坏我的现有代码吗?
不会自动破坏——这点可以放心。对于已有项目,Approachable Concurrency 是可选启用的,你得手动在 Build Settings 中开启才行。新建项目则是默认启用。不过一旦启用,nonisolated 异步函数的运行位置会改变(从全局执行器变成调用者的 Actor),所以迁移前务必审查那些依赖后台执行的代码。
@concurrent 和 Task.detached 有什么区别?
@concurrent 标记在函数声明上,表示该函数始终在全局并发执行器上运行——这是函数的固有属性。而 Task.detached 是在调用侧创建一个脱离当前 Actor 上下文的新任务。前者更简洁、意图更明确;后者更灵活,但容易丢失结构化并发的好处。在 Swift 6.2 中,绝大多数场景都应该优先用 @concurrent。
启用 MainActor 默认隔离后,SwiftUI 代码需要改动吗?
基本不需要。SwiftUI 的 View 协议从 iOS 18 起就已经隐式标记为 @MainActor 了,所以你的 View 层本来就在主线程上。启用默认隔离后,之前手动加的那些 @MainActor 注解变成了冗余——可以删掉让代码更干净,当然留着也不碍事。主要的改动集中在 ViewModel 和 Service 层:检查是否有依赖后台执行的 nonisolated async 方法,该加 @concurrent 的加上。
nonisolated(nonsending) 函数中的 await 会阻塞主线程吗?
不会,这点很重要。await 是一个挂起点(suspension point),执行到 await 时函数会挂起当前任务并释放线程。比如 await URLSession.shared.data(from: url) 在等网络响应的时候,主线程是完全空闲的,可以正常处理 UI 事件。真正会占用主线程的是 await 前后的同步代码——如果那部分有 CPU 密集型计算,那才需要考虑用 @concurrent。
我应该先迁移到 Swift 6.0 还是直接跳到 Swift 6.2?
如果你的项目还在 Swift 5 上,我的建议是直接上 Swift 6.2。Swift 6.0 的严格并发检查对很多项目来说实在太激进了(我见过上千个 Sendable 错误的场景),而 Swift 6.2 的 Approachable Concurrency 让这个过渡平滑了很多。先启用 Swift 6 语言模式,然后立刻开启 Approachable Concurrency 来减少编译器错误——这是目前最省心的迁移路径。