Swift 并发编程

深入探讨 Swift 6 结构化并发,涵盖 Async/Await、Task Group 以及解决数据竞争的 Actors 模型。

掌握从 GCD 到现代 Structured Concurrency 的范式演进,构建高性能、线程安全的 iOS 应用。


1. 结构化并发 (Structured Concurrency)

在 Swift 5.5+ 中,Apple 引入了全新的结构化并发模型。与传统的 GCD 不同,异步任务现在拥有了明确的父子关系生命周期管理

核心优势

  • 自动取消传播:当父任务被取消时,所有子任务自动取消
  • 资源自动释放:任务完成后,相关资源自动清理
  • 编译时安全检查:编译器会检测潜在的数据竞争

Async/Await 基础

通过 async 标识函数为异步,await 标识潜在挂起点。代码读起来像同步代码一样流畅:

func fetchUserData() async throws -> User {
    let url = URL(string: "https://api.example.com/user")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

// 调用异步函数
Task {
    do {
        let user = try await fetchUserData()
        print("用户名: \(user.name)")
    } catch {
        print("获取失败: \(error)")
    }
}

并行执行多个任务

使用 async let 可以并行执行多个独立的异步操作:

func fetchDashboardData() 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
    )
}

TaskGroup 动态并发

当任务数量不确定时,使用 TaskGroup

func fetchAllImages(urls: [URL]) async throws -> [UIImage] {
    try await withThrowingTaskGroup(of: UIImage.self) { group in
        for url in urls {
            group.addTask {
                let (data, _) = try await URLSession.shared.data(from: url)
                guard let image = UIImage(data: data) else {
                    throw ImageError.invalidData
                }
                return image
            }
        }
        
        var images: [UIImage] = []
        for try await image in group {
            images.append(image)
        }
        return images
    }
}

2. Actor 模型

Actor 是 Swift 5.5 引入的新引用类型,通过数据隔离确保线程安全。

基本使用

actor BankAccount {
    private var balance: Double = 0
    
    func deposit(_ amount: Double) {
        balance += amount
    }
    
    func withdraw(_ amount: Double) throws -> Double {
        guard balance >= amount else {
            throw BankError.insufficientFunds
        }
        balance -= amount
        return amount
    }
    
    func getBalance() -> Double {
        return balance
    }
}

// 使用 Actor
let account = BankAccount()

Task {
    await account.deposit(100)
    let balance = await account.getBalance()
    print("余额: \(balance)")
}

Actor 隔离规则

  • 内部访问:Actor 内部方法可以直接访问属性
  • 外部访问:必须使用 await 访问 Actor 的属性和方法
  • nonisolated:标记不需要隔离的方法
actor DataManager {
    private var cache: [String: Data] = [:]
    
    // 需要隔离的方法
    func store(_ data: Data, for key: String) {
        cache[key] = data
    }
    
    // 不需要隔离的方法(只读计算属性)
    nonisolated var description: String {
        return "DataManager Instance"
    }
}

@MainActor - 主线程隔离

用于确保 UI 操作在主线程执行:

@MainActor
class ViewModel: ObservableObject {
    @Published var items: [Item] = []
    @Published var isLoading = false
    
    func loadItems() async {
        isLoading = true
        defer { isLoading = false }
        
        do {
            items = try await APIService.fetchItems()
        } catch {
            print("加载失败: \(error)")
        }
    }
}

3. 任务取消与超时

检查取消状态

func processLargeDataset(_ data: [Item]) async throws -> [Result] {
    var results: [Result] = []
    
    for item in data {
        // 检查是否被取消
        try Task.checkCancellation()
        
        // 或者优雅地处理取消
        if Task.isCancelled {
            // 清理资源
            break
        }
        
        let result = await process(item)
        results.append(result)
    }
    
    return results
}

任务超时

func fetchWithTimeout() async throws -> Data {
    try await withThrowingTaskGroup(of: Data.self) { group in
        group.addTask {
            try await fetchData()
        }
        
        group.addTask {
            try await Task.sleep(nanoseconds: 5_000_000_000) // 5秒
            throw TimeoutError.exceeded
        }
        
        // 返回第一个完成的结果
        let result = try await group.next()!
        group.cancelAll()
        return result
    }
}

4. Sendable 协议

Sendable 标记类型可以安全地跨并发域传递:

// 值类型自动 Sendable
struct UserData: Sendable {
    let id: UUID
    let name: String
}

// 类需要显式标记并确保线程安全
final class Configuration: Sendable {
    let apiKey: String  // 只有 let 属性
    let baseURL: URL
    
    init(apiKey: String, baseURL: URL) {
        self.apiKey = apiKey
        self.baseURL = baseURL
    }
}

// Actor 自动 Sendable
actor Counter: Sendable {
    private var count = 0
    func increment() { count += 1 }
}

5. 性能对比

维度 GCD Swift Concurrency
线程模型 每个任务可能新建线程 固定的协作式线程池
上下文切换 频繁,开销大 轻量级任务切换
内存安全 需手动处理 Data Race 编译器强制检查
取消机制 需手动实现 内置结构化取消
代码可读性 嵌套回调 线性同步风格
调试体验 堆栈难以追踪 完整的异步堆栈

6. 最佳实践

✅ 推荐做法

// 1. 使用 async/await 替代回调
func loadData() async throws -> Data {
    try await URLSession.shared.data(from: url).0
}

// 2. 使用 Actor 保护共享状态
actor Cache {
    private var storage: [String: Data] = [:]
    func get(_ key: String) -> Data? { storage[key] }
    func set(_ key: String, data: Data) { storage[key] = data }
}

// 3. 在视图模型中使用 @MainActor
@MainActor
class ViewModel: ObservableObject {
    @Published var data: [Item] = []
}

❌ 避免的做法

// 1. 不要在 Actor 中执行长时间同步操作
actor BadActor {
    func processSync() {
        // ❌ 这会阻塞 Actor
        Thread.sleep(forTimeInterval: 5)
    }
}

// 2. 不要忽略取消检查
func badLoop() async {
    for i in 0..<1000000 {
        // ❌ 没有检查取消状态
        await heavyWork(i)
    }
}

// 3. 不要过度使用 Task.detached
Task.detached {
    // ❌ 失去结构化并发的优势
}

7. 迁移指南

从 GCD 迁移到 Swift Concurrency:

// 旧代码 - GCD
func fetchUser(completion: @escaping (Result<User, Error>) -> Void) {
    DispatchQueue.global().async {
        // 网络请求...
        DispatchQueue.main.async {
            completion(.success(user))
        }
    }
}

// 新代码 - Swift Concurrency
func fetchUser() async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

兼容性桥接

使用 withCheckedContinuation 包装旧的回调 API:

func fetchUserAsync() async throws -> User {
    try await withCheckedThrowingContinuation { continuation in
        fetchUser { result in
            continuation.resume(with: result)
        }
    }
}