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

7 阅读6分钟

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 类型

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

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 三步走,每步修完警告再推进。


参考资料