Swift 新并发框架之 async/await

1. 为什么需要 async/await

在移动开发里,"并发/异步"几乎无处不在:网络请求、图片下载、文件读写、数据库操作......它们都有一个共同特点:

  • 耗时(如果你在主线程里死等,会卡 UI)
  • 结果稍后才回来(你必须用某种方式"拿到结果")

传统的并发模型大多是"回调式"的:用 completion、delegate、通知等在未来某个时间点把结果交还给你。 这套方案能跑,但会带来两个典型挑战(你参考资料里也点出来了):

  1. 代码维护性差:异步回调让代码阅读"跳来跳去",不线性
  2. 容易出现数据竞争(Data Races):共享可变状态在并发下很容易出 Bug,而且难复现、难排查

Swift 从 5.5 开始引入新并发框架,async/await 先解决第一个核心痛点:让异步代码看起来像同步代码一样顺畅,并且把错误处理变得更自然。


2. 先复习 4 个基础概念

2.1 同步 vs 异步(描述"函数怎么返回")

  • 同步(Synchronous) :函数执行完才返回 例子:let x = add(1, 2),拿到结果才能往下走
  • 异步(Asynchronous) :函数把任务"丢出去",结果以后再给你 所以你必须用 completion / delegate / async/await 等方式拿结果

2.2 串行 vs 并行/并发(描述"一组任务怎么跑")

  • 串行(Serial):一次只执行一个任务,完成一个再下一个
  • 并发/并行(Concurrent/Parallel):同一段时间内可能执行多个任务 (本文不严格区分并发与并行;你只要知道:会"同时处理多个任务"就行)

3. 回调式异步的典型痛点:回调地狱 + 错误处理难看

用"头像加载"来说明: 步骤:拿 URL → 下载数据(加密)→ 解密 → 解码成图片

3.1 回调地狱(Callback Hell)

swift 复制代码
class AvatarLoader {
    func loadAvatar(token: String, completion: @escaping (UIImage) -> Void) {
        fetchAvatarURL(token: token) { url in
            self.fetchAvatar(url: url) { data in
                self.decryptAvatar(data: data) { data in
                    self.decodeImage(data: data) { image in
                        completion(image)
                    }
                }
            }
        }
    }

    func fetchAvatarURL(token: String, completion: @escaping (String) -> Void) { /* ... */ }
    func fetchAvatar(url: String, completion: @escaping (Data) -> Void) { /* ... */ }
    func decryptAvatar(data: Data, completion: @escaping (Data) -> Void) { /* ... */ }
    func decodeImage(data: Data, completion: @escaping (UIImage) -> Void) { /* ... */ }
}

阅读体验像在"走迷宫":你要不停地进回调、出回调,脑子要同时记住很多上下文。

3.2 错误处理会让代码更糟

异步回调常见的写法是 completion(value, error) 或者 Result。 但在多层嵌套里,错误传递会越来越啰嗦,漏掉某个分支的 completion 也很常见。


4. async/await 的核心:把"异步代码"写成"线性代码"

4.1 先看效果:一眼就能读懂

swift 复制代码
class AvatarLoader {
    func loadAvatar(token: String) async throws -> UIImage {
        let url = try await fetchAvatarURL(token: token)
        let encryptedData = try await fetchAvatar(url: url)
        let decryptedData = try await decryptAvatar(data: encryptedData)
        return try await decodeImage(data: decryptedData)
    }

    func fetchAvatarURL(token: String) async throws -> String { /* ... */ }
    func fetchAvatar(url: String) async throws -> Data { /* ... */ }
    func decryptAvatar(data: Data) async throws -> Data { /* ... */ }
    func decodeImage(data: Data) async throws -> UIImage { /* ... */ }
}

你会发现:

  • 代码像同步一样从上到下执行("直线式")
  • 错误处理回到了熟悉的 throws + try + do/catch
  • 逻辑清晰、可维护性大幅提升

5. 语法规则:你只需要掌握这 3 条

5.1 async:声明"这个函数可能会挂起"

swift 复制代码
func fetch() async -> Int { 42 }

含义:它是"异步函数",执行过程中可能暂停(挂起),等待某些事情完成(比如网络返回、IO 完成)。

5.2 await:调用 async 函数时必须写

swift 复制代码
let value = await fetch()

await 表示:这里是一个潜在挂起点(potential suspension point)。 意思是:运行到这里,可能会"先停一下",等结果准备好再继续往下走。

5.3 await 只能出现在"异步上下文"里

异步上下文主要有两类:

  1. async 函数体内部
  2. Task 闭包内部

6. 一个关键认知:挂起的是"函数",不是"线程"

这是很多新手最容易误解的地方:

  • await 不是"把当前线程卡住等待"
  • 它是"把当前函数挂起",让出执行权
  • 等条件满足后,再恢复执行(恢复时可能换了线程

你可以把它想象成:

你在排队取号(await),你人可以先离开去做别的(线程去执行别的任务),等叫到你号了你再回来继续办理(函数恢复执行)。

结论:在 async/await 的世界里,别强依赖"我现在一定在某个线程上"。


7. 为什么"锁 + await"容易出事:一个经典坑

一个很典型的示例:

swift 复制代码
let lock = NSLock()

func test() async {
    lock.lock()
    try? await Task.sleep(nanoseconds: 1_000_000_000)
    lock.unlock()
}

for _ in 0..<10 {
    Task { await test() }
}

问题在哪里?

  • lock.lock() 后遇到了 await
  • 函数可能挂起并发生线程切换
  • 其他任务也想拿锁,但锁可能被"拿着不放"
  • 结果就是:很容易出现死锁/饥饿等难排查问题

经验法则:

不要在持有锁的期间跨过 await。 如果你需要保护共享可变状态,优先考虑 actor或让状态只在单一执行上下文里修改。


8. 真正能上手的代码:用 URLSession 写一个 async 网络请求

iOS 15+(Swift 5.5+)开始,URLSession 已经提供了 async API,例如:

swift 复制代码
let (data, response) = try await URLSession.shared.data(from: url)

我们做一个"获取用户信息"的示例(包含错误处理):

8.1 定义模型与错误

swift 复制代码
import Foundation

struct User: Decodable {
    let id: Int
    let name: String
}

enum NetworkError: Error {
    case invalidURL
    case invalidResponse
    case httpStatus(Int)
}

8.2 写一个 async 网络层

swift 复制代码
final class APIClient {
    func fetchUser(id: Int) async throws -> User {
        guard let url = URL(string: "https://example.com/users/\(id)") else {
            throw NetworkError.invalidURL
        }

        let (data, response) = try await URLSession.shared.data(from: url)

        guard let http = response as? HTTPURLResponse else {
            throw NetworkError.invalidResponse
        }
        guard (200...299).contains(http.statusCode) else {
            throw NetworkError.httpStatus(http.statusCode)
        }

        return try JSONDecoder().decode(User.self, from: data)
    }
}

这一段代码的阅读体验就是"同步式"的:构建 URL → await 拿数据 → 校验 → decode。


9. 在 UIViewController 里怎么调用 async 并更新 UI?

这是 iOS 开发里最常见的落地问题:

async 方法不能直接在普通函数里 await,那我怎么从按钮点击里发请求?

答案:用 Task { } 把它放进异步上下文里。

9.1 示例:点击按钮加载用户并刷新 label

swift 复制代码
import UIKit

final class UserViewController: UIViewController {

    private let api = APIClient()
    private let label = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white

        label.numberOfLines = 0
        label.frame = CGRect(x: 20, y: 100, width: 320, height: 200)
        view.addSubview(label)

        loadUser()
    }

    private func loadUser() {
        // 进入异步上下文
        Task { [weak self] in
            guard let self else { return }

            do {
                let user = try await api.fetchUser(id: 1)

                // 更新 UI:回到主线程(MainActor)
                await MainActor.run {
                    self.label.text = "用户:\(user.name)\nID:\(user.id)"
                }

            } catch {
                await MainActor.run {
                    self.label.text = "加载失败:\(error)"
                }
            }
        }
    }
}

你只要记住一句话就够了:

  • 耗时工作 放在 Taskawait
  • UI 更新 放在 MainActor(主线程语义)里做

10. 从旧代码迁移:把 completion 回调包装成 async

很多项目里已经有大量回调式 API,你不可能一夜之间全改掉。Swift 提供了"续体(Continuation)"做桥接。

10.1 假设你有旧接口

swift 复制代码
final class LegacyService {
    func fetchText(completion: @escaping (Result<String, Error>) -> Void) {
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            completion(.success("hello async/await"))
        }
    }
}

10.2 用 withCheckedThrowingContinuation 包装

swift 复制代码
extension LegacyService {
    func fetchText() async throws -> String {
        try await withCheckedThrowingContinuation { continuation in
            fetchText { result in
                switch result {
                case .success(let text):
                    continuation.resume(returning: text)
                case .failure(let error):
                    continuation.resume(throwing: error)
                }
            }
        }
    }
}

10.3 使用方式

swift 复制代码
Task {
    do {
        let text = try await LegacyService().fetchText()
        print(text)
    } catch {
        print("error:", error)
    }
}

11. 入门阶段的最佳实践清单

  1. 不要在持锁期间跨 await(容易死锁/逻辑卡住)
  2. UI 更新统一回到 MainActor(避免主线程问题)
  3. 能用 throws 就别用 Optional+error 乱传:错误路径更清晰
  4. 从入口处就结构化async 函数调用 async 函数,别层层回调
  5. 迁移旧代码用 Continuation:逐步改,不要一次性重构到崩

12. 小结

到这里,你已经具备了 async/await 的"可用级理解":

  • async:这个函数可能挂起
  • await:潜在挂起点,只能在异步上下文使用
  • async/await 让异步代码"线性可读",错误处理回到 throws
  • 挂起的是函数,不是线程;await 前后可能换线程
  • iOS 里用 Task {} 进入异步上下文,用 MainActor 更新 UI
  • 旧回调接口可以用 withCheckedThrowingContinuation 平滑迁移

相关推荐
TheNextByte12 小时前
如何将文件从iPhone传输到USB闪存盘?
ios·iphone
lzhdim2 小时前
iPhone 18系列明年Q1试产:首发A20系列芯片
ios·iphone
long_run3 小时前
iOS 开发:Objective-C 之字典对象
ios
long_run3 小时前
iOS 开发:Objective-C 之分类及协议
ios
图图大恼3 小时前
iOS Objective-C 协议一致性检查:从基础到优化的完整解决方案
ios·objective-c·apple
柯南二号4 小时前
【大前端】【iOS】iOS 使用 Objective-C 绘制几大常见布局(UIKit / Core Graphics 实战)
前端·ios
LorrestGump19 小时前
手游 iOS SDK 改用 CocoaPods 集成
ios
TheNextByte11 天前
如何通过 4 种简单方法克隆 iPhone
ios·iphone
2501_916007471 天前
iOS 证书如何创建,从能生成到能长期使用
android·macos·ios·小程序·uni-app·cocoa·iphone