ios开发方向——swift并发进阶核心 async/await 详解

Swift async/await 全面详解

async/await 是 Swift 5.5 引入的结构化并发核心语法,彻底重构了 Swift 的异步编程范式,让异步代码可以像同步代码一样线性书写,消除了传统回调嵌套(回调地狱),同时提供编译期安全检查、统一错误处理和自动化的任务生命周期管理,是当前 Apple 平台推荐的异步编程标准方案Apple Developer。

一、核心基础语法

1. async:标记异步函数

async 关键字用于标记一个函数 / 方法 / 计算属性为异步函数,声明该函数具备「挂起 - 恢复」的能力,执行过程中可以主动让出线程,等待异步操作完成后再恢复执行。

语法规则
  • 位置:函数参数括号之后,throws 之前(如有),返回值之前
  • 异步函数只能在异步上下文中被调用
  • 支持与 throws 结合,实现异步错误抛出
Swift 复制代码
// 基础异步函数
func fetchData(from url: URL) async -> Data {
    // 异步操作实现
}

// 可抛出错误的异步函数(最常用)
func fetchUser() async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: userURL)
    return try JSONDecoder().decode(User.self, from: data)
}

2. await:标记挂起点

await 关键字用于标记异步函数的调用点,这里是潜在的挂起点:执行到此处时,当前任务会被挂起,让出线程资源给其他任务,直到异步函数执行完成,再恢复后续代码的执行。

核心特点
  • 挂起≠阻塞:挂起时线程不会被锁死,会被 Swift 运行时回收,调度执行其他任务,完全避免了线程阻塞的性能损耗
  • 仅标记潜在挂起:如果异步操作已完成,会直接同步执行后续代码,不会真的挂起
  • 必须与 async 配对:只能在 async 标记的异步上下文中使用
调用示例
Swift 复制代码
// 异步函数中调用
func loadUserInfo() async {
    do {
        // 调用异步函数,标记挂起点,等待结果返回后再往下执行
        let user = try await fetchUser()
        print("用户信息:\(user)")
        // 后续业务逻辑
    } catch {
        print("请求失败:\(error)")
    }
}

// 同步上下文调用:必须用Task创建异步执行环境
// 比如UIKit的按钮点击事件(同步函数)中调用异步代码
@IBAction func loadButtonTapped(_ sender: UIButton) {
    Task {
        await loadUserInfo()
    }
}

二、核心优势:对比传统回调式编程

传统异步编程依赖 GCD 的 completion handler 回调,极易出现多层嵌套的「回调地狱」,代码可读性差、错误处理分散、容易遗漏回调调用,而 async/await 彻底解决了这些问题。

对比示例:网络请求嵌套

1. 传统 GCD 回调写法
Swift 复制代码
// 回调式API
func fetchUser(completion: @escaping (Result<User, Error>) -> Void) {
    URLSession.shared.dataTask(with: userURL) { data, _, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        guard let data = data else {
            completion(.failure(FetchError.noData))
            return
        }
        do {
            let user = try JSONDecoder().decode(User.self, from: data)
            completion(.success(user))
        } catch {
            completion(.failure(error))
        }
    }.resume()
}

// 调用:多层嵌套回调地狱
fetchUser { result in
    switch result {
    case .success(let user):
        // 嵌套请求用户头像
        fetchAvatar(userId: user.id) { avatarResult in
            switch avatarResult {
            case .success(let avatar):
                // 嵌套切回主线程更新UI
                DispatchQueue.main.async {
                    self.avatarImageView.image = avatar
                }
            case .failure(let error):
                print("头像加载失败:\(error)")
            }
        }
    case .failure(let error):
        print("用户加载失败:\(error)")
    }
}
2. async/await 写法
Swift 复制代码
// async/await 风格API
func fetchUser() async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: userURL)
    return try JSONDecoder().decode(User.self, from: data)
}

func fetchAvatar(userId: String) async throws -> UIImage {
    let (data, _) = try await URLSession.shared.data(from: avatarURL(userId: userId))
    guard let image = UIImage(data: data) else {
        throw FetchError.invalidImage
    }
    return image
}

// 调用:线性书写,无嵌套,逻辑清晰
func loadUserAvatar() async {
    do {
        // 顺序执行,先获取用户,再获取头像
        let user = try await fetchUser()
        let avatar = try await fetchAvatar(userId: user.id)
        // 主线程更新UI
        await MainActor.run {
            self.avatarImageView.image = avatar
        }
    } catch {
        // 统一错误处理
        print("加载失败:\(error)")
    }
}

核心优势总结

  1. 代码线性书写:彻底消除嵌套回调,逻辑顺序与代码执行顺序完全一致,可读性和可维护性大幅提升
  2. 统一错误处理 :与同步代码完全一致的 try/do-catch 机制,无需在每个回调中单独处理错误,避免遗漏
  3. 编译期安全保障:编译器强制要求处理错误,杜绝了回调式 API 中「忘记调用 completion」的致命 bug
  4. 自动化生命周期管理:基于结构化并发,任务有明确的层级和作用域,父任务会自动等待子任务完成,避免内存泄漏和任务失控
  5. 极低的调度开销:基于协程实现,挂起 / 恢复仅在用户态完成,无需内核态的线程切换,性能远超传统线程切换

三、底层执行原理

1. 协程与状态机

Swift 的 async/await 基于协程(Coroutine) 实现,编译器会将异步函数转换为一个状态机,管理函数的挂起与恢复:

  • 每个 await 挂起点对应一个状态,编译器会自动保存当前函数的执行上下文(局部变量、程序计数器、栈帧等)
  • 挂起时,上下文被保存到堆中,线程被释放回全局协作线程池,执行其他任务
  • 异步操作完成后,系统恢复保存的上下文,从挂起的状态继续执行后续代码

2. 协作式线程池

Swift 并发系统内置了一个全局协作线程池,线程数量与设备 CPU 核心数匹配,彻底避免了 GCD 中常见的「线程爆炸」问题:

  • 线程池仅负责执行就绪的异步任务,任务挂起时立即回收线程,最大化 CPU 资源利用率
  • 开发者无需手动管理线程、队列,所有调度都由 Swift 运行时自动完成
  • 保证同一时间不会有超过 CPU 核心数的线程处于运行状态,减少内核态的线程调度开销

3. 挂起 vs 阻塞的核心区别

行为 特点 线程状态 资源占用
await 挂起 协作式让出线程,保存上下文,可恢复 线程被释放,执行其他任务 极低,仅保存上下文
线程阻塞 强制占用线程,等待事件完成 线程休眠,无法执行其他任务 高,持续占用线程资源

四、进阶核心用法

1. 并行执行异步任务

对于无依赖的多个异步任务,使用 async let 可以实现并行执行,总耗时等于耗时最长的单个任务,而非所有任务耗时之和,大幅提升执行效率。

Swift 复制代码
// 串行执行:总耗时 = 任务1耗时 + 任务2耗时
let user = try await fetchUser()
let articles = try await fetchArticles()

// 并行执行:总耗时 = max(任务1耗时, 任务2耗时)
// async let 立即启动异步任务,不会阻塞当前执行
async let user = fetchUser()
async let articles = fetchArticles()
// 此处await等待两个任务全部完成
let (fetchedUser, fetchedArticles) = try await (user, articles)

2. 任务取消与协作式取消

Swift 结构化并发支持协作式取消:父任务取消时,会自动向所有子任务传播取消信号,子任务可以检查取消状态,提前终止执行。

核心用法
Swift 复制代码
// 支持取消的异步函数
func fetchLargeFile() async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: largeFileURL)
    
    // 检查任务是否被取消,已取消则抛出CancellationError
    try Task.checkCancellation()
    
    // 也可以手动判断取消状态
    if Task.isCancelled {
        throw CancellationError()
    }
    
    return processLargeData(data)
}

// 手动取消任务
let downloadTask = Task {
    try await fetchLargeFile()
}

// 触发取消
downloadTask.cancel()

3. 适配传统回调式 API

对于老的基于 completion handler 的 API,可以通过 withCheckedThrowingContinuation(支持错误)/ withCheckedContinuation(无错误) 快速封装为 async/await 风格的 API。

封装规则
  • 续体(continuation)必须且只能 resume 一次,多次调用会触发运行时错误
  • 无论成功 / 失败,都必须调用 resume,否则会导致任务永久挂起
Swift 复制代码
// 传统回调API
func legacyFetchUser(userId: String, completion: @escaping (Result<User, Error>) -> Void) {
    // 原有实现
}

// 封装为async/await API
func fetchUser(userId: String) async throws -> User {
    try await withCheckedThrowingContinuation { continuation in
        legacyFetchUser(userId: userId) { result in
            switch result {
            case .success(let user):
                continuation.resume(returning: user)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

4. 主线程调度与 MainActor

UI 相关操作必须在主线程执行,Swift 并发模型通过 @MainActor 提供编译期的主线程保障,替代传统的 DispatchQueue.main.async

核心用法
Swift 复制代码
// 标记整个类/结构体的所有方法、属性都在主线程执行
@MainActor
class UserProfileViewModel: ObservableObject {
    @Published var avatar: UIImage?
    
    func loadAvatar() async throws {
        let user = try await fetchUser()
        let avatar = try await fetchAvatar(userId: user.id)
        // 无需手动切主线程,@MainActor保证此处在主线程执行
        self.avatar = avatar
    }
}

// 标记单个函数在主线程执行
@MainActor
func updateAvatarUI(_ image: UIImage) {
    avatarImageView.image = image
}

// 异步函数中调用主线程方法
func loadUserAvatar() async {
    do {
        let user = try await fetchUser()
        let avatar = try await fetchAvatar(userId: user.id)
        // 等待主线程执行完成
        await updateAvatarUI(avatar)
    } catch {
        print(error)
    }
}

五、系统版本与兼容性

  • 原生支持:Swift 5.5+,iOS 15+/macOS 12+/watchOS 8+/tvOS 15+
  • 回退支持:Xcode 13.2+,Swift 5.5+,可向下兼容到 iOS 13+/macOS 10.15+(部分高级特性受限)
  • 系统 API 适配:Apple 官方框架(URLSession、UIKit、HealthKit 等)已全面提供 async/await 版本的 API,无需手动封装Apple Developer

六、最佳实践与注意事项

  1. 禁止在 async 函数中执行长时间同步阻塞操作全局协作线程池的线程数量有限,长时间阻塞线程会导致线程池无法调度其他任务,引发性能问题甚至死锁。耗时的同步操作应单独封装,避免占用并发线程池。

  2. 优先使用结构化并发,避免滥用 Task.detached Task.detached 会创建脱离父任务层级的分离任务,无法自动传播取消、无法自动等待完成,极易导致任务失控和内存泄漏,优先使用 Taskasync letTaskGroup 等结构化并发 API。

  3. UI 操作必须使用 MainActor 保障主线程执行 所有视图更新、UI 事件处理都必须在主线程执行,优先使用 @MainActor 标记相关类型和方法,避免手动切换主线程导致的错误。

  4. 主动响应任务取消 耗时较长的异步函数中,应在关键节点通过 Task.checkCancellation() 检查取消状态,及时释放资源、终止执行,避免无效的资源占用。

  5. 避免过度创建并行任务 虽然 async letTaskGroup 支持并行执行,但大量并行任务会导致系统资源耗尽,应根据业务场景控制并行任务的数量。

相关推荐
青花瓷2 小时前
采用QT下MingW编译opencv4.8.1
开发语言·qt
开心就好20252 小时前
HTTPS超文本传输安全协议全面解析与工作原理
后端·ios
赫瑞2 小时前
Java中的日期类
java·开发语言
吕司2 小时前
Linux线程同步
linux·服务器·开发语言
神の愛2 小时前
java日志功能
java·开发语言·前端
Reuuse2 小时前
基于 C++ 的网页五子棋对战项目实战
开发语言·c++
不会写DN2 小时前
如何设计应用层 ACK 来补充 TCP 的不足?
开发语言·网络·数据库·网络协议·tcp/ip·golang
xyq20242 小时前
PHP MySQL 简介
开发语言
我能坚持多久2 小时前
利用Date类的实现对知识巩固与自省
开发语言·c++