Swift 新并发框架之 async/await 详解

0 阅读8分钟

Swift 新并发框架之 async/await 详解

目录


概述

Swift 5.5 引入了全新的并发模型,其中 async/await 是核心特性。这个新框架提供了一种更简洁、更安全的方式来处理异步代码,解决了传统回调地狱(Callback Hell)和复杂的 GCD 代码问题。

关键特性

  • async/await: 异步函数声明和等待
  • Task: 结构化并发任务
  • Actor: 数据隔离和线程安全
  • AsyncSequence: 异步序列
  • TaskGroup: 并发任务组

为什么需要新的并发框架

传统异步编程的痛点

1. 回调地狱(Callback Hell)
// 传统回调方式 - 嵌套层级深,难以维护
func fetchUserData(completion: @escaping (Result<User, Error>) -> Void) {
    fetchUserID { result in
        switch result {
        case .success(let userID):
            fetchUserProfile(userID: userID) { result in
                switch result {
                case .success(let profile):
                    fetchUserPosts(userID: userID) { result in
                        switch result {
                        case .success(let posts):
                            let user = User(profile: profile, posts: posts)
                            completion(.success(user))
                        case .failure(let error):
                            completion(.failure(error))
                        }
                    }
                case .failure(let error):
                    completion(.failure(error))
                }
            }
        case .failure(let error):
            completion(.failure(error))
        }
    }
}
2. 错误处理复杂
  • 每层回调都需要处理错误
  • 错误传递路径不清晰
  • 容易遗漏错误处理
3. 线程管理困难
  • GCD 的队列管理复杂
  • 容易出现数据竞争
  • 难以追踪执行上下文
4. 代码可读性差
  • 执行顺序不直观
  • 难以理解程序流程
  • 维护成本高

async/await 基础

声明异步函数

// 使用 async 关键字声明异步函数
func fetchData() async -> Data {
    // 异步操作
    return data
}

// 可以抛出错误的异步函数
func fetchData() async throws -> Data {
    // 可能失败的异步操作
    return data
}

调用异步函数

// 使用 await 关键字等待异步函数完成
func loadUserData() async {
    let data = await fetchData()
    print("数据加载完成: \(data)")
}

// 处理可能抛出错误的异步函数
func loadUserData() async throws {
    do {
        let data = try await fetchData()
        print("数据加载完成: \(data)")
    } catch {
        print("加载失败: \(error)")
    }
}

核心概念

1. Task - 异步任务

Task 是 Swift 并发的基本执行单元。

// 创建独立任务
Task {
    let result = await fetchData()
    print(result)
}

// 带优先级的任务
Task(priority: .high) {
    await performCriticalOperation()
}

// 取消任务
let task = Task {
    await longRunningOperation()
}
task.cancel()

// 检查任务是否被取消
func processItems() async {
    for item in items {
        if Task.isCancelled {
            print("任务已取消")
            return
        }
        await process(item)
    }
}

2. Actor - 数据隔离

Actor 提供了线程安全的数据访问机制。

actor BankAccount {
    private var balance: Double = 0
    
    func deposit(amount: Double) {
        balance += amount
    }
    
    func withdraw(amount: Double) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }
    
    func getBalance() -> Double {
        return balance
    }
}

// 使用 Actor
let account = BankAccount()

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

3. AsyncSequence - 异步序列

处理异步数据流。

// 自定义异步序列
struct AsyncCountdown: AsyncSequence {
    typealias Element = Int
    let start: Int
    
    struct AsyncIterator: AsyncIteratorProtocol {
        var current: Int
        
        mutating func next() async -> Int? {
            guard current >= 0 else { return nil }
            let value = current
            current -= 1
            try? await Task.sleep(nanoseconds: 1_000_000_000) // 1秒
            return value
        }
    }
    
    func makeAsyncIterator() -> AsyncIterator {
        return AsyncIterator(current: start)
    }
}

// 使用异步序列
for await number in AsyncCountdown(start: 5) {
    print(number)
}

4. TaskGroup - 并发任务组

同时执行多个任务并收集结果。

func fetchMultipleUsers(ids: [Int]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask {
                try await fetchUser(id: id)
            }
        }
        
        var users: [User] = []
        for try await user in group {
            users.append(user)
        }
        return users
    }
}

实战示例

示例 1: 网络请求

传统方式(URLSession + Completion Handler)
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    
    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        
        guard let data = data else {
            completion(.failure(NetworkError.noData))
            return
        }
        
        do {
            let user = try JSONDecoder().decode(User.self, from: data)
            completion(.success(user))
        } catch {
            completion(.failure(error))
        }
    }.resume()
}

// 使用
fetchUser(id: 1) { result in
    switch result {
    case .success(let user):
        print("用户: \(user.name)")
    case .failure(let error):
        print("错误: \(error)")
    }
}
async/await 方式
func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    let user = try JSONDecoder().decode(User.self, from: data)
    return user
}

// 使用
Task {
    do {
        let user = try await fetchUser(id: 1)
        print("用户: \(user.name)")
    } catch {
        print("错误: \(error)")
    }
}

示例 2: 串行异步操作

传统方式
func loadUserProfile(completion: @escaping (Result<UserProfile, Error>) -> Void) {
    fetchUserID { result in
        switch result {
        case .success(let userID):
            fetchUserData(userID: userID) { result in
                switch result {
                case .success(let userData):
                    fetchUserAvatar(userID: userID) { result in
                        switch result {
                        case .success(let avatar):
                            let profile = UserProfile(data: userData, avatar: avatar)
                            completion(.success(profile))
                        case .failure(let error):
                            completion(.failure(error))
                        }
                    }
                case .failure(let error):
                    completion(.failure(error))
                }
            }
        case .failure(let error):
            completion(.failure(error))
        }
    }
}
async/await 方式
func loadUserProfile() async throws -> UserProfile {
    let userID = try await fetchUserID()
    let userData = try await fetchUserData(userID: userID)
    let avatar = try await fetchUserAvatar(userID: userID)
    return UserProfile(data: userData, avatar: avatar)
}

// 使用
Task {
    do {
        let profile = try await loadUserProfile()
        print("用户资料加载完成")
    } catch {
        print("加载失败: \(error)")
    }
}

示例 3: 并行异步操作

传统方式(使用 DispatchGroup)
func loadDashboardData(completion: @escaping (Result<DashboardData, Error>) -> Void) {
    let group = DispatchGroup()
    var userData: User?
    var posts: [Post]?
    var notifications: [Notification]?
    var error: Error?
    
    group.enter()
    fetchUser { result in
        switch result {
        case .success(let user):
            userData = user
        case .failure(let err):
            error = err
        }
        group.leave()
    }
    
    group.enter()
    fetchPosts { result in
        switch result {
        case .success(let fetchedPosts):
            posts = fetchedPosts
        case .failure(let err):
            error = err
        }
        group.leave()
    }
    
    group.enter()
    fetchNotifications { result in
        switch result {
        case .success(let fetchedNotifications):
            notifications = fetchedNotifications
        case .failure(let err):
            error = err
        }
        group.leave()
    }
    
    group.notify(queue: .main) {
        if let error = error {
            completion(.failure(error))
        } else if let userData = userData, let posts = posts, let notifications = notifications {
            let dashboard = DashboardData(user: userData, posts: posts, notifications: notifications)
            completion(.success(dashboard))
        } else {
            completion(.failure(NetworkError.unknown))
        }
    }
}
async/await 方式
func loadDashboardData() async throws -> DashboardData {
    async let user = fetchUser()
    async let posts = fetchPosts()
    async let notifications = fetchNotifications()
    
    // 并行执行,等待所有结果
    return try await DashboardData(
        user: user,
        posts: posts,
        notifications: notifications
    )
}

// 使用
Task {
    do {
        let dashboard = try await loadDashboardData()
        print("仪表盘数据加载完成")
    } catch {
        print("加载失败: \(error)")
    }
}

示例 4: 图片下载与缓存

actor ImageCache {
    private var cache: [URL: UIImage] = [:]
    
    func image(for url: URL) -> UIImage? {
        return cache[url]
    }
    
    func store(_ image: UIImage, for url: URL) {
        cache[url] = image
    }
}

class ImageLoader {
    private let cache = ImageCache()
    
    func loadImage(from url: URL) async throws -> UIImage {
        // 检查缓存
        if let cachedImage = await cache.image(for: url) {
            print("从缓存加载图片")
            return cachedImage
        }
        
        // 下载图片
        print("从网络下载图片")
        let (data, _) = try await URLSession.shared.data(from: url)
        
        guard let image = UIImage(data: data) else {
            throw ImageError.invalidData
        }
        
        // 存入缓存
        await cache.store(image, for: url)
        
        return image
    }
}

// 使用
let loader = ImageLoader()
Task {
    do {
        let url = URL(string: "https://example.com/image.jpg")!
        let image = try await loader.loadImage(from: url)
        print("图片加载完成: \(image.size)")
    } catch {
        print("加载失败: \(error)")
    }
}

示例 5: 实时数据流处理

// 模拟实时数据流
struct StockPriceStream: AsyncSequence {
    typealias Element = Double
    let symbol: String
    
    struct AsyncIterator: AsyncIteratorProtocol {
        let symbol: String
        
        mutating func next() async throws -> Double? {
            // 模拟每秒更新一次股价
            try await Task.sleep(nanoseconds: 1_000_000_000)
            return Double.random(in: 100...200)
        }
    }
    
    func makeAsyncIterator() -> AsyncIterator {
        return AsyncIterator(symbol: symbol)
    }
}

// 使用异步流
func monitorStockPrice(symbol: String) async throws {
    let stream = StockPriceStream(symbol: symbol)
    
    for try await price in stream {
        print("\(symbol) 当前价格: $\(String(format: "%.2f", price))")
        
        // 价格预警
        if price > 180 {
            print("⚠️ 价格过高警告!")
        }
        
        // 检查任务是否被取消
        if Task.isCancelled {
            print("停止监控")
            break
        }
    }
}

// 启动监控
let task = Task {
    try await monitorStockPrice(symbol: "AAPL")
}

// 5秒后取消
Task {
    try await Task.sleep(nanoseconds: 5_000_000_000)
    task.cancel()
}

示例 6: 文件批量处理

func processFiles(urls: [URL]) async throws -> [ProcessedFile] {
    try await withThrowingTaskGroup(of: ProcessedFile.self) { group in
        for url in urls {
            group.addTask {
                print("开始处理: \(url.lastPathComponent)")
                let data = try await loadFile(from: url)
                let processed = try await processData(data)
                print("完成处理: \(url.lastPathComponent)")
                return ProcessedFile(url: url, data: processed)
            }
        }
        
        var results: [ProcessedFile] = []
        for try await result in group {
            results.append(result)
        }
        return results
    }
}

func loadFile(from url: URL) async throws -> Data {
    try await Task.sleep(nanoseconds: 1_000_000_000) // 模拟 I/O
    return Data()
}

func processData(_ data: Data) async throws -> Data {
    try await Task.sleep(nanoseconds: 500_000_000) // 模拟处理
    return data
}

// 使用
Task {
    let urls = [
        URL(fileURLWithPath: "/path/file1.txt"),
        URL(fileURLWithPath: "/path/file2.txt"),
        URL(fileURLWithPath: "/path/file3.txt")
    ]
    
    do {
        let results = try await processFiles(urls: urls)
        print("所有文件处理完成,共 \(results.count) 个")
    } catch {
        print("处理失败: \(error)")
    }
}

与传统异步方案对比

1. Completion Handler vs async/await

特性Completion Handlerasync/await
代码可读性❌ 嵌套深,难以阅读✅ 线性流程,清晰直观
错误处理❌ 每层都需要处理✅ 统一的 try-catch
内存管理⚠️ 容易循环引用✅ 自动管理
调试难度❌ 堆栈追踪困难✅ 堆栈清晰
编译器检查⚠️ 检查有限✅ 强类型检查

2. GCD vs Task

特性GCDTask
线程管理❌ 手动管理队列✅ 自动调度
取消操作❌ 需要手动实现✅ 内置取消机制
优先级⚠️ 队列优先级✅ 任务优先级
结构化并发❌ 不支持✅ 原生支持
数据竞争❌ 容易出现✅ Actor 保护

3. DispatchGroup vs TaskGroup

// DispatchGroup - 复杂且容易出错
func loadDataWithDispatchGroup(completion: @escaping ([Data]) -> Void) {
    let group = DispatchGroup()
    var results: [Data] = []
    let lock = NSLock()
    
    for i in 0..<5 {
        group.enter()
        fetchData(id: i) { data in
            lock.lock()
            results.append(data)
            lock.unlock()
            group.leave()
        }
    }
    
    group.notify(queue: .main) {
        completion(results)
    }
}

// TaskGroup - 简洁且类型安全
func loadDataWithTaskGroup() async throws -> [Data] {
    try await withThrowingTaskGroup(of: Data.self) { group in
        for i in 0..<5 {
            group.addTask {
                try await fetchData(id: i)
            }
        }
        
        var results: [Data] = []
        for try await data in group {
            results.append(data)
        }
        return results
    }
}

优缺点分析

async/await 的优点

1. ✅ 代码可读性显著提升
// 清晰的顺序执行流程
func updateUserProfile() async throws {
    let user = try await fetchUser()
    let profile = try await fetchProfile(for: user)
    let avatar = try await downloadAvatar(url: profile.avatarURL)
    try await saveProfile(profile, avatar: avatar)
}
2. ✅ 错误处理统一
// 使用标准的 do-catch-throw 模式
do {
    let result = try await riskyOperation()
    print("成功: \(result)")
} catch {
    print("失败: \(error)")
}
3. ✅ 自动内存管理
  • 不需要使用 [weak self][unowned self]
  • 避免循环引用问题
  • 编译器自动管理生命周期
4. ✅ 编译时安全检查
// 编译器会强制要求使用 await
func getData() async -> Data {
    return await fetchData() // 必须使用 await,否则编译错误
}
5. ✅ 结构化并发
// 任务自动继承上下文和优先级
Task {
    async let a = fetchA()
    async let b = fetchB()
    let results = try await [a, b]
}
6. ✅ 更好的调试体验
  • 清晰的调用堆栈
  • 断点调试更直观
  • Instruments 工具支持更好
7. ✅ 线程安全
// Actor 提供自动的数据隔离
actor Counter {
    private var value = 0
    
    func increment() {
        value += 1 // 自动线程安全
    }
}

async/await 的缺点

1. ❌ 学习曲线
  • 需要理解新的并发模型
  • Actor、Task、AsyncSequence 等新概念
  • 与旧代码混合使用时需要适配
2. ❌ 系统版本要求
  • iOS 15.0+ / macOS 12.0+
  • 无法在旧系统上使用
  • 需要考虑向后兼容性
// 需要版本检查
if #available(iOS 15.0, *) {
    Task {
        await newAsyncFunction()
    }
} else {
    // 使用旧的异步方式
    oldCallbackFunction { result in
        // ...
    }
}
3. ❌ 与旧代码集成需要桥接
// 需要手动桥接旧的 completion handler
func legacyFetch(completion: @escaping (Data?, Error?) -> Void) {
    // 旧代码
}

// 桥接到 async/await
func modernFetch() async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        legacyFetch { data, error in
            if let error = error {
                continuation.resume(throwing: error)
            } else if let data = data {
                continuation.resume(returning: data)
            } else {
                continuation.resume(throwing: NetworkError.unknown)
            }
        }
    }
}
4. ❌ 性能开销
  • Task 创建有轻微开销
  • Actor 的隔离机制有性能成本
  • 对于极简单的异步操作可能过度设计
5. ⚠️ 调试工具仍在完善
  • Xcode 的并发调试工具还在改进
  • 某些场景下的错误信息不够清晰
  • 性能分析工具支持有限
6. ⚠️ 生态系统迁移中
  • 许多第三方库仍在适配
  • 部分框架同时提供两套 API
  • 文档和最佳实践还在积累

最佳实践

1. 合理使用 async let

// ✅ 好的做法 - 并行执行独立任务
func loadDashboard() async throws -> Dashboard {
    async let user = fetchUser()
    async let posts = fetchPosts()
    async let stats = fetchStats()
    
    return try await Dashboard(user: user, posts: posts, stats: stats)
}

// ❌ 不好的做法 - 有依赖关系时不应使用 async let
func loadUserPosts() async throws -> [Post] {
    async let user = fetchUser()
    async let posts = fetchPosts(userId: user.id) // 错误!user 还未就绪
    return try await posts
}

// ✅ 正确做法 - 串行执行有依赖的任务
func loadUserPosts() async throws -> [Post] {
    let user = try await fetchUser()
    let posts = try await fetchPosts(userId: user.id)
    return posts
}

2. 使用 TaskGroup 处理动态数量的任务

// ✅ 使用 TaskGroup 处理可变数量的任务
func downloadImages(urls: [URL]) async throws -> [UIImage] {
    try await withThrowingTaskGroup(of: (Int, UIImage).self) { group in
        for (index, url) in urls.enumerated() {
            group.addTask {
                let image = try await downloadImage(from: url)
                return (index, image)
            }
        }
        
        var images = [UIImage?](repeating: nil, count: urls.count)
        for try await (index, image) in group {
            images[index] = image
        }
        return images.compactMap { $0 }
    }
}

3. 正确处理任务取消

func longRunningTask() async throws {
    for i in 0..<1000 {
        // 定期检查取消状态
        try Task.checkCancellation()
        
        // 或者使用
        if Task.isCancelled {
            print("任务被取消,清理资源")
            cleanup()
            return
        }
        
        await processItem(i)
    }
}

4. 使用 Actor 保护共享状态

// ✅ 使用 Actor 保护共享数据
actor DataStore {
    private var cache: [String: Data] = [:]
    
    func getData(key: String) -> Data? {
        return cache[key]
    }
    
    func setData(_ data: Data, key: String) {
        cache[key] = data
    }
}

// ❌ 不要在多个任务间直接共享可变状态
class UnsafeDataStore {
    var cache: [String: Data] = [:] // 数据竞争风险!
}

5. 避免在 Actor 中执行耗时同步操作

actor FileProcessor {
    // ❌ 不好的做法 - 阻塞 Actor
    func processFile(_ url: URL) -> Data {
        let data = try! Data(contentsOf: url) // 同步 I/O,会阻塞
        return process(data)
    }
    
    // ✅ 好的做法 - 使用异步操作
    func processFile(_ url: URL) async throws -> Data {
        let data = try await loadFile(url) // 异步 I/O
        return process(data)
    }
}

6. 合理设置任务优先级

// 用户交互 - 高优先级
Task(priority: .userInitiated) {
    let result = await fetchCriticalData()
    updateUI(with: result)
}

// 后台任务 - 低优先级
Task(priority: .background) {
    await syncDataToServer()
}

7. 使用 MainActor 更新 UI

@MainActor
class ViewModel: ObservableObject {
    @Published var data: [Item] = []
    
    func loadData() async {
        let items = await fetchItems()
        // 自动在主线程执行
        self.data = items
    }
}

// 或者显式标记
func updateUI() async {
    await MainActor.run {
        label.text = "更新完成"
    }
}

8. 桥接旧代码

// 将 completion handler 转换为 async/await
func fetchData() async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        legacyFetchData { data, error in
            if let error = error {
                continuation.resume(throwing: error)
            } else if let data = data {
                continuation.resume(returning: data)
            }
        }
    }
}

9. 避免过度并发

// ❌ 不好的做法 - 创建过多任务
for i in 0..<10000 {
    Task {
        await process(i)
    }
}

// ✅ 好的做法 - 限制并发数量
func processBatch(_ items: [Item], maxConcurrency: Int = 5) async {
    await withTaskGroup(of: Void.self) { group in
        var iterator = items.makeIterator()
        
        // 启动初始任务
        for _ in 0..<maxConcurrency {
            if let item = iterator.next() {
                group.addTask { await process(item) }
            }
        }
        
        // 当任务完成时,启动新任务
        for await _ in group {
            if let item = iterator.next() {
                group.addTask { await process(item) }
            }
        }
    }
}

10. 测试异步代码

import XCTest

class AsyncTests: XCTestCase {
    func testAsyncFunction() async throws {
        let result = try await fetchData()
        XCTAssertNotNil(result)
    }
    
    func testTaskCancellation() async throws {
        let task = Task {
            try await longRunningOperation()
        }
        
        task.cancel()
        
        do {
            _ = try await task.value
            XCTFail("应该抛出取消错误")
        } catch is CancellationError {
            // 预期的取消错误
        }
    }
}

总结

何时使用 async/await

推荐使用的场景:

  • 新项目或新功能
  • 复杂的异步流程
  • 需要多个异步操作协调
  • 需要更好的代码可维护性
  • 目标系统版本支持(iOS 15+)

⚠️ 谨慎使用的场景:

  • 需要支持旧系统版本
  • 极简单的异步操作
  • 性能极其敏感的场景
  • 团队不熟悉新并发模型

关键要点

  1. async/await 让异步代码看起来像同步代码,大幅提升可读性
  2. Actor 提供了线程安全的数据隔离,避免数据竞争
  3. 结构化并发确保任务的生命周期管理更清晰
  4. 需要 iOS 15+,考虑向后兼容性
  5. 与旧代码集成需要桥接,但值得投入
  6. 编译器提供更强的类型检查,减少运行时错误

Swift 的新并发框架代表了现代异步编程的方向,虽然有学习成本和系统要求,但带来的代码质量和开发体验提升是值得的。对于新项目,强烈建议采用 async/await;对于现有项目,可以逐步迁移关键模块。


参考资源: