2、async/await基础:从零开始理解异步函数

好,咱们正式开始聊 async/await。说实话,Swift 5.5 刚推出这个特性时,我第一反应是「终于来了」。之前用回调闭包写异步代码,那叫一个酸爽——嵌套多了,代码读起来像俄罗斯套娃。我自己维护过一个老项目,里面有个三层回调嵌套的网络请求,每次改需求都得盯着屏幕看半天,生怕漏掉某个闭包里的 return。

async/await 的出现,说白了就是让异步代码写起来像同步代码一样直白。你不需要再手动管理 DispatchQueue,也不用担心回调地狱。咱们今天就把这块地基打牢。

2.1 定义 async 函数

定义一个异步函数,只需要在函数声明里加上 async 关键字。我习惯把它放在参数列表后面、箭头前面:

func fetchUserData() async -> User? {
    // 这里可以执行异步操作
    return nil
}

注意看,async 是函数类型的一部分。这意味着你不能把一个异步函数赋值给一个同步类型的变量。嗯,这里要注意——编译器会严格检查。

异步函数可以抛出错误,写法也很自然:

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

我个人建议:只要函数内部涉及网络请求、文件读写、或者任何可能阻塞的操作,就把它标记为 async。别偷懒,这不仅是规范问题,更是对调用者负责。

2.2 使用 await 调用异步函数

调用异步函数时,必须在前面加上 await 关键字。这就像在说:「嘿,编译器,这里可能会暂停,等我一下」。

let user = await fetchUserData()
print("用户数据:\(user?.name ?? "未知")")

你可能会问:为什么非要写 await?不能省略吗?

答案是不能。这是 Swift 设计上的一个精妙之处——await 是一个显式的「暂停点」标记。读代码的人一眼就能看出哪里可能会挂起。我在 Code Review 时,经常靠这个标记快速定位潜在的并发问题。

关键点await 只能在异步上下文中使用。也就是说,你必须在 async 函数里,或者放在 Task { } 块里才能调用 await。

2.3 异步函数的执行流程

理解执行流程,是掌握 async/await 的核心。咱们用一个简单的例子来拆解:

func step1() async -> Int {
    print("步骤1开始")
    try? await Task.sleep(nanoseconds: 1_000_000_000) // 模拟耗时1秒
    print("步骤1结束")
    return 1
}

func step2() async -> Int {
    print("步骤2开始")
    try? await Task.sleep(nanoseconds: 500_000_000) // 模拟耗时0.5秒
    print("步骤2结束")
    return 2
}

func process() async {
    let result1 = await step1()
    let result2 = await step2()
    print("最终结果:\(result1 + result2)")
}

执行流程是这样的:

  1. process() 开始执行,遇到 await step1()
  2. 程序暂停 process() 的执行,转而执行 step1()
  3. step1() 内部遇到 await Task.sleep,再次暂停
  4. 此时线程可以去做其他事情(比如处理 UI 事件)
  5. 1秒后,step1() 恢复执行,返回结果
  6. process() 拿到结果,继续执行到 await step2()
  7. 重复上述暂停-恢复流程

说白了,await 就是一个「让出线程」的信号。线程不会傻等着,而是去干别的活。等异步操作完成了,再回来接着干。这就是 Swift 并发的高效之处。

小技巧:如果你想让两个异步任务并行执行,不要连续写两个 await。应该用 async let 或者 TaskGroup。这个咱们后面章节会详细讲。

2.4 异步函数与同步函数的互调

这是实际开发中最容易踩坑的地方。我总结成三种情况:

情况一:同步函数调用异步函数

直接调用是不行的,编译器会报错:

func syncFunction() {
    // 错误:'async' call in a function that does not support concurrency
    let user = await fetchUserData()
}

解决办法有两个:

  • 把同步函数改成 async 函数(推荐)
  • Task { } 包裹(慎用,有生命周期问题)
func syncFunction() {
    Task {
        let user = await fetchUserData()
        print(user?.name ?? "")
    }
    // 注意:Task 里的代码是异步执行的,syncFunction 不会等它
}

我曾经在 ViewController 的 viewDidLoad 里直接用 Task 调异步方法,结果页面都销毁了,回调才回来,导致野指针崩溃。嗯,血的教训。

情况二:异步函数调用同步函数

这个完全没问题,异步函数天然可以调用同步函数:

func syncHelper() -> String {
    return "同步计算结果"
}

func asyncProcess() async -> String {
    let result = syncHelper() // 直接调用,没问题
    let data = await fetchData()
    return result + data
}

但要注意:不要在异步函数里长时间执行同步阻塞操作。比如在 async 函数里调用 Thread.sleep() 或者执行一个耗时的 for 循环,这会阻塞整个线程,破坏并发的优势。

情况三:从闭包中调用异步函数

这是最隐蔽的坑。看这个例子:

func processItems(_ items: [Int]) async {
    items.forEach { item in
        // 错误:'async' call in a closure that does not support concurrency
        let result = await processItem(item)
    }
}

forEach 的闭包不是异步上下文,所以不能直接调用 await。解决办法是用 for-in 循环代替:

func processItems(_ items: [Int]) async {
    for item in items {
        let result = await processItem(item)
        // 处理 result
    }
}

避坑指南:我曾经在项目里用 map + Task 的方式并行处理数组,结果因为闭包捕获了外部变量,导致数据竞争。后来全部改成 for-in + TaskGroup 才解决。记住:闭包里的异步调用,一定要确认上下文是否正确。

2.5 总结一下

今天咱们聊了 async/await 的四个基础知识点:

知识点 一句话总结
定义 async 函数 在函数声明里加 async,可能抛出错误就加 throws
使用 await 调用 每个异步调用前加 await,标记暂停点
执行流程 遇到 await 就暂停,让出线程,完成后恢复
同步异步互调 同步调异步用 Task 包裹,异步调同步直接调但要小心阻塞

下一章咱们会深入 async let 和结构化并发,到时候你会发现,今天这些基础就像搭积木的底座——底座稳了,上面才能盖高楼。

记住:写异步代码,心里要时刻想着「这里会不会暂停?」、「暂停后线程去干嘛了?」。保持这个意识,你很快就能写出健壮的并发代码。