Swift Concurrency 实战指南

async/await、Actor、Structured Concurrency……Swift 5.5 引入的并发特性如何在大项目中安全使用?本文分享我们的实战经验和踩坑记录。

Swift 5.5 引入的 async/await 彻底改变了 iOS 开发者的异步编程方式。从最初的回调地狱,到 GCD 的.DispatchQueue,再到今天的 Structured Concurrency,我们终于有了更安全、更易读的并发工具。

但新特性也带来了新的陷阱。本文分享我们在多个项目中使用 Swift Concurrency 的实战经验。

async/await 基础

如果你还没用过 async/await,先看个简单示例:

// 老方式:Completion Handler
func fetchUser(id: String, completion: (Result<User, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        // 处理结果
    }.resume()
}

// 新方式:async/await
func fetchUser(id: String) async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

代码从"金字塔"变成线性结构,可读性大幅提升。

Task 与结构化并发

Task是 Swift Concurrency 的基本单位:

Task {
    let user = try await fetchUser(id: "123")
    await MainActor.run {
        self.userNameLabel.stringValue = user.name
    }
}

关键原则

Actor 隔离

Actor 用于保护可变状态,避免数据竞争:

actor UserCache {
    private var cache: [String: User] = [:]

    func get(_ id: String) -> User? { cache[id] }
    func set(_ user: User) { cache[user.id] = user }
}

访问 actor 的属性自动需要 await

let user = await cache.get("123")

MainActor

UI 操作必须在主线程:

@MainActor
class ViewModel {
    func updateUI() {
        // 自动在主线程执行
    }
}

// 或者在方法级别标注
@MainActor
func updateLabel() { ... }

实战场景

1. 并行请求

需要同时请求多个独立数据时,用 async let

func loadDashboard() async throws -> Dashboard {
    async let user = fetchUser()
    async let posts = fetchPosts()
    async let notifications = fetchNotifications()

    return try await Dashboard(
        user: user,
        posts: posts,
        notifications: notifications
    )
}

比串行执行快得多。

2. 超时处理

withTimeout 避免无限等待:

try await withTimeout(after: 5) {
    try await fetchUser(id: "123")
} catch {
    // 处理超时
}

3. 重试逻辑

网络请求失败时自动重试:

func fetchWithRetry<T>(
    operation: () async throws -> T,
    maxRetries: Int = 3
) async throws -> T {
    var lastError: Error?

    for attempt in 0..<maxRetries {
        do {
            return try await operation()
        } catch {
            lastError = error
            try await Task.sleep(nanoseconds: 1_000_000_000 * (attempt + 1))
        }
    }
    throw lastError!
}

常见陷阱

1. 忘记处理取消

长时间运行的任务应该检查取消:

func processData(_ items: [Item]) async throws {
    for item in items {
        try Task.checkCancellation() // 检查是否被取消
        try await processOne(item)
    }
}

2. 死锁

在 Actor 内部调用需要 await 的方法可能导致死锁:

actor MyActor {
    func doSomething() async {
        // 错误:在 actor 内部等待自己
        await self.doSomethingElse()
    }
}

3. 过度使用 MainActor

不是所有 ViewModel 方法都需要 @MainActor,纯数据处理不需要:

class ViewModel {
    // 不需要 MainActor
    func transformData(_ input: Data) -> Output { ... }

    // 需要 MainActor(更新 UI 状态)
    @MainActor
    func updateStatus(_ status: String) { ... }
}

迁移建议

如果项目还在用 completion handler:

  1. 先从新代码开始用 async/await
  2. Task 包装老代码
  3. 逐步重构老代码,优先重构测试代码
  4. 最后处理复杂的依赖场景

总结

Swift Concurrency 是 iOS 开发的重大进步:

正确使用这些工具,代码会更安全、更易读、更高效。