Swift 6 严格并发检查实战:从警告到零错误的完整迁移指南

20 阅读10分钟

Swift 6 严格并发检查实战:从警告到零错误的完整迁移指南

本文是 Swift Concurrency 系列第五章,聚焦 Swift 6 严格并发检查(Strict Concurrency Checking)——从语言演进背景、核心机制、常见编译错误到实战修复策略,帮你平稳迁出"数据竞争地带"。


一、Swift 版本迭代简史

在深入 Swift 6 之前,先回顾一下 Swift 的演进脉络,了解"严格并发"为何成为这一版本的最大主题。

版本年份核心主题
Swift 1.02014发布,面向对象 + 函数式,ARC 内存管理
Swift 2.02015错误处理(do/try/catch),Protocol Extensions,开源
Swift 3.02016命名规范大重构(API Guidelines),Linux 支持
Swift 4.02017Codable,String 改进,键路径(Key Path)
Swift 4.22018CaseIterable,Random API 统一
Swift 5.02019ABI 稳定!Result 类型,字符串插值重构
Swift 5.12019SwiftUI 发布,@propertyWrapper,Opaque Return Types
Swift 5.52021async/await,Actor,Sendable(并发奠基版)
Swift 5.72022if let 简写,Clock/Duration,Regex Literal
Swift 5.92023if/switch 表达式,Macros(宏系统),Parameter Packs
Swift 5.102024完整并发检查预览,全局变量隔离警告
Swift 6.02024严格并发检查强制开启,数据竞争编译期保证

为什么 Swift 6 是里程碑?

Swift 5.5 引入了 async/awaitActor,但并发检查默认是宽松的——大量数据竞争隐患不会报错。Swift 6 将其升级为编译错误,Apple 的目标是:

"Swift 6 语言模式使并发安全成为编译期保证,而不仅仅是最佳实践。"


二、什么是严格并发检查?

2.1 三种检查模式

Swift 提供三个并发检查级别(在 Xcode Build Settings 或 Package.swift 中配置):

SWIFT_STRICT_CONCURRENCY = minimal     // 默认(Swift 5 行为)
SWIFT_STRICT_CONCURRENCY = targeted    // 中间档:对已标注的代码检查
SWIFT_STRICT_CONCURRENCY = complete    // Swift 6 行为(最严格)

Package.swift 中:

.target(
    name: "MyApp",
    swiftSettings: [
        .enableExperimentalFeature("StrictConcurrency")   // Swift 5.9+
        // 或直接设置语言版本
    ]
)

开启 Swift 6 模式(Xcode 16+):

// Swift Language Version = Swift 6
// 或 Package.swift
.target(
    swiftLanguageVersions: [.version("6")]
)

2.2 核心保证:消灭数据竞争

严格并发检查的本质是禁止跨并发域的可变状态共享。编译器会强制检查:

  1. Sendable 合规性:跨 actor/任务边界传递的类型必须是 Sendable
  2. Actor 隔离:访问 actor 的属性/方法必须在 actor 上下文中
  3. 全局状态隔离:全局/静态变量必须是不可变或被 actor 隔离
  4. 闭包捕获@Sendable 闭包中不能捕获非 Sendable 类型

2.3 四大核心检查规则深度解析

规则一:Sendable 合规性 — 跨线程传数据必须有"通行证"

跨越 Task / actor 边界的每个对象,必须能证明"我被多个线程同时持有不会出问题"。

// ❌ 场景:把购物车传给后台 Task 做价格计算
class ShoppingCart {
    var items: [String] = []
    var totalPrice: Double = 0
}

let cart = ShoppingCart()
Task.detached {
    // Swift 6 Error: Sending 'cart' risks causing data races
    // 主线程可能同时修改 cart,这里也在读 → 数据竞争
    print(cart.totalPrice)
}

// ✅ 修复 A:改成 struct(值类型,传递时自动拷贝)
struct ShoppingCart: Sendable {
    var items: [String] = []
    var totalPrice: Double = 0
}
// Task 持有的是一份拷贝,主线程修改原件互不影响

// ✅ 修复 B:改成 actor(同一份对象,但访问串行化)
actor ShoppingCart {
    var items: [String] = []
    var totalPrice: Double = 0
}

记忆口诀:Sendable 是"安全通行证",没通行证的对象不能过并发边界。


规则二:Actor 隔离 — 访问 actor 的属性/方法必须 await

actor 内部有一个隐式的"串行队列",任何外部访问都必须排队(await)。

actor UserCenter {
    var nickname = "游客"
    var isLoggedIn = false

    func login(name: String) {
        nickname = name
        isLoggedIn = true
    }
}

let center = UserCenter()

// ❌ 直接访问会报错(在 actor 外部)
// print(center.nickname)  // Error: Expression is 'async' but is not marked with 'await'

// ✅ 必须 await,保证串行
Task {
    await center.login(name: "Tom")
    let name = await center.nickname
    print(name)  // Tom
}

// 两个任务并发操作 → actor 自动排队,结果确定
Task { await center.login(name: "Alice") }
Task { await center.login(name: "Bob") }
// 结果一定是 Alice 或 Bob,不会产生混乱的中间状态

记忆口诀:actor 像"单窗口银行柜台",不管多少人同时来,只服务一个,其余排队。


规则三:全局状态隔离 — 全局变量必须是 let 或被 actor 保护

全局 var 是"公共厕所"——多个线程随时可以进,必然出问题。

// ❌ Swift 6 Error: Global variable is not concurrency-safe
var currentUser: String = "guest"
var imageCache: [String: Data] = [:]

// ✅ 方案一:改成 let(不可变,没有竞争)
let appVersion: String = "1.0.0"

// ✅ 方案二:@MainActor 隔离(UI 相关全局状态)
@MainActor var currentUser: String = "guest"
// 只有主线程能读写,SwiftUI 天然安全

// ✅ 方案三:封装进 actor(后台缓存,推荐)
actor ImageCache {
    static let shared = ImageCache()
    private var storage: [String: Data] = [:]

    func store(_ data: Data, key: String) { storage[key] = data }
    func fetch(_ key: String) -> Data? { storage[key] }
}

// 使用:任何线程调用都安全
Task { await ImageCache.shared.store(data, key: "avatar") }

记忆口诀:全局变量要么"锁门"(let),要么"派管理员"(actor),要么"指定专属厕所"(@MainActor)。


规则四:闭包捕获 — @Sendable 闭包不能捕获非 Sendable 类型

@Sendable 闭包需要跨线程传递,因此它"打包带走"的东西必须是安全的(Sendable)。

// ❌ ViewModel 是普通 class,不是 Sendable
class FeedViewModel {
    var posts: [String] = []

    func loadPosts() {
        Task.detached {
            // Swift 6 Error: Capture of 'self' with non-sendable type
            // 'FeedViewModel' in a @Sendable closure
            self.posts = ["post1", "post2"]  // ❌
        }
    }
}

// ✅ 修复:加 @MainActor,整个类在主线程隔离
@MainActor
class FeedViewModel: ObservableObject {
    @Published var posts: [String] = []

    func loadPosts() {
        Task {
            // 继承 @MainActor,self 的访问安全
            let data = await fetchFromServer()  // 后台执行
            posts = data                         // ✅ 回到主线程赋值
        }
    }

    private func fetchFromServer() async -> [String] {
        try? await Task.sleep(for: .seconds(1))
        return ["post1", "post2"]
    }
}

记忆口诀@Sendable 闭包是"密封快递盒",装进去的东西必须是密封安全的,不能装"会漏水"的对象。


四大规则总结
线程 A ←──── 并发边界 ────→ 线程 B / Task / Actor

传递对象?  → 规则一:必须是 Sendable(有通行证)
访问属性?  → 规则二:必须在 actor 上下文中 await(排队)
全局变量?  → 规则三:let / @MainActor / actor 三选一
闭包捕获?  → 规则四:只能捕获 Sendable 类型(密封打包)
规则本质问题核心解法
Sendable 合规class 被多线程共享引用改 struct / actor / @unchecked
Actor 隔离无保护访问 actor 内部强制 await 串行访问
全局状态隔离全局 var 任意线程可写let / @MainActor / actor
闭包捕获闭包跨线程持有非安全对象@MainActor class / Sendable 类型

四条规则解决的是同一件事:不让两个线程同时"摸"同一块可变内存。


三、常见编译错误与修复实战

3.1 错误一:非 Sendable 类型跨越并发边界

问题代码:

class UserSession {        // ❌ 未标注 Sendable
    var token: String = ""
    var userId: Int = 0
}

let session = UserSession()

Task {
    // ❌ Swift 6 Error: Sending 'session' risks causing data races
    print(session.token)
}

修复方案 A:改为 struct(值语义,自动 Sendable)

struct UserSession: Sendable {  // ✅ struct 默认满足 Sendable
    var token: String = ""
    var userId: Int = 0
}

修复方案 B:class 显式声明 + 加锁(不推荐新代码)

final class UserSession: @unchecked Sendable {  // ⚠️ 需自己保证线程安全
    private let lock = NSLock()
    private var _token: String = ""
    var token: String {
        get { lock.withLock { _token } }
        set { lock.withLock { _token = newValue } }
    }
}

修复方案 C(推荐):改为 Actor

actor UserSession {             // ✅ Actor 天然 Sendable + 自动隔离
    var token: String = ""
    var userId: Int = 0
    
    func updateToken(_ newToken: String) {
        token = newToken
    }
}

// 使用
let session = UserSession()
Task {
    await session.updateToken("abc123")
    let t = await session.token
    print(t)
}

3.2 错误二:全局可变变量

问题代码:

var globalCache: [String: Data] = [:]   // ❌ Swift 6 Error: Global variable is not concurrency-safe

修复方案 A:用 @MainActor 隔离

@MainActor
var globalCache: [String: Data] = [:]   // ✅ 只在主线程访问

// 访问时需在主线程上下文
Task { @MainActor in
    globalCache["key"] = Data()
}

修复方案 B:用 nonisolated(unsafe) 标注(慎用)

nonisolated(unsafe) var globalCache: [String: Data] = [:]
// ⚠️ 告诉编译器"我保证安全,请不要检查"

修复方案 C:封装进 Actor

actor CacheManager {
    static let shared = CacheManager()
    private var storage: [String: Data] = [:]
    
    func set(_ data: Data, forKey key: String) {
        storage[key] = data
    }
    
    func get(forKey key: String) -> Data? {
        storage[key]
    }
}

3.3 错误三:@Sendable 闭包捕获

问题代码:

class ViewModel: ObservableObject {
    var items: [String] = []
    
    func loadData() {
        Task {
            // ❌ Capture of 'self' with non-sendable type 'ViewModel' in a @Sendable closure
            self.items = ["a", "b", "c"]
        }
    }
}

修复方案:@MainActor + ObservableObject

@MainActor
class ViewModel: ObservableObject {   // ✅ @MainActor 隔离整个类
    @Published var items: [String] = []
    
    func loadData() {
        Task {
            let result = await fetchRemote()   // 可跨线程执行
            items = result                     // ✅ 自动回到 MainActor
        }
    }
    
    private func fetchRemote() async -> [String] {
        try? await Task.sleep(for: .seconds(1))
        return ["a", "b", "c"]
    }
}

3.4 错误四:Protocol 中的并发要求

问题代码:

protocol DataLoader {
    func load() async -> [String]
}

// ❌ 在某些上下文中 conformance 需要 Sendable
class NetworkLoader: DataLoader {
    func load() async -> [String] { ... }
}

修复方案:

// 方案 A:Protocol 本身要求 Sendable
protocol DataLoader: Sendable {
    func load() async -> [String]
}

// 方案 B:存在类型约束
protocol DataLoader {
    func load() async -> [String]
}

// 使用时加约束
func use<T: DataLoader & Sendable>(_ loader: T) async { ... }

3.5 错误五:@preconcurrency 渐进迁移

当你依赖的旧模块还没有迁移到 Swift 6 时,可以用 @preconcurrency 临时抑制警告:

// 导入尚未迁移的模块
@preconcurrency import SomeOldFramework

// 或在 Protocol Conformance 上
class MyView: @preconcurrency UIViewController {
    // ...
}

四、nonisolated 关键字详解

nonisolated 用于在 Actor 中声明某个方法不需要 actor 隔离,可以从任意上下文调用:

actor DataStore {
    var data: [String] = []
    
    // ❌ 必须 await 调用
    func getData() -> [String] { data }
    
    // ✅ nonisolated:无需 await,可同步调用
    nonisolated var description: String {
        "DataStore instance"   // 只能访问不可变 or nonisolated 的属性
    }
    
    // nonisolated 的方法不能访问 actor 隔离的属性
    nonisolated func staticInfo() -> String {
        "Version 1.0"          // ✅ 只访问常量/外部数据
    }
}

let store = DataStore()
print(store.description)        // ✅ 无需 await
print(await store.getData())    // 需要 await

五、@MainActor 传播规则

@MainActor 会沿着调用链传播,理解这点对排错非常重要:

@MainActor
class HomeViewController: UIViewController {
    
    // 所有方法自动在主线程
    func updateUI() {
        label.text = "Updated"      // ✅
    }
    
    // 如需后台工作,用 nonisolated 或 Task.detached
    nonisolated func heavyWork() async -> String {
        // 在非主线程执行
        return "result"
    }
    
    func loadAndUpdate() {
        Task {
            let result = await heavyWork()   // 后台
            updateUI()                        // 自动回主线程(因为在 @MainActor 上下文)
        }
    }
}

传播场景总结:

场景结论
@MainActor class 内部的方法自动继承 @MainActor
@MainActor 函数内 await仍在主线程
Task { }@MainActor 上下文创建继承主线程隔离
Task.detached { }不继承,独立调度
nonisolated 方法脱离 actor 隔离

六、渐进迁移策略

Apple 官方推荐的迁移路径(不用一次跳到 Swift 6):

第一步:开启 targeted 检查

SWIFT_STRICT_CONCURRENCY = targeted

只检查已有 async/awaitActor 标注的代码,低风险。

第二步:修复所有警告

重点关注:

  • Sendable 警告
  • nonisolated 相关警告
  • @MainActor 遗漏

第三步:升级到 complete 检查

SWIFT_STRICT_CONCURRENCY = complete

这时会暴露更多隐藏问题,逐一解决。

第四步:切换到 Swift 6 语言版本

SWIFT_VERSION = 6

所有并发检查变为编译错误,不再是警告。


七、实战 Checklist

开启 Swift 6 前,用这份清单扫描你的代码:

  • 全局/静态 var → 改为 letnonisolated(unsafe)、或 @MainActor
  • class 跨线程传递 → 加 Sendable、改 struct、或改 actor
  • ObservableObject 子类 → 加 @MainActor
  • Delegate 回调 → 检查是否在主线程,用 @MainActor 声明 Delegate protocol
  • 第三方库 → 用 @preconcurrency import 过渡
  • 闭包捕获 → 检查 [weak self] + @Sendable 约束

八、小结

Swift 6 的严格并发检查不是为了让你"多写几个关键字",而是把数据竞争这类运行时 Bug 提前消灭在编译阶段。迁移的过程可能短暂痛苦,但带来的收益是:

  • 🔒 零数据竞争的类型系统保证
  • 🚀 更好的并发性能(编译器可以更大胆优化)
  • 🧠 代码意图更清晰(谁在哪个线程做什么,一目了然)

迁移建议:不要急于一次切换到 Swift 6 模式,按 minimal → targeted → complete 三步走,每步修完警告再推进。


参考资料