首页 / Swift 编程语言教程 / 并发基础与 async/await

Swift 编程语言教程

并发基础与 async/await

本教程共 93 篇 · 第 80 篇 · 更新于 2026-08-08 · 约 9 分钟阅读

SwiftSwift 编程语言教程并发asyncawait异步函数挂起结构化并发

本节目标:理解 Swift 的 async/await 怎么让异步代码写起来像同步代码,搞清楚挂起与恢复,以及异步函数该在哪儿被调用。

并发是 Swift 现代语法里最值得花时间的一块。它要解决的核心问题是:当程序在等网络、等磁盘、等耗时计算时,怎么不让整个界面卡住。Swift 用 async/await 给出了一套清晰、安全、好读的写法。

1-1 什么叫异步代码

同步函数要么跑完、要么抛错、要么不返回,是一条道走到黑的。异步函数特殊一点:它跑到一半可能因为”在等东西”而挂起(suspend),把当前线程让出来给别人用,等东西齐了再恢复(resume)继续跑。

打个比方:你排队点咖啡,点完单拿到小票后不用干站原地等,可以先去占座、回消息。咖啡好了店员喊你,你再回来取。挂起就是”拿着小票去干别的”,恢复就是”被喊到回来取咖啡”。这期间你(线程)没被白占着。

Swift 的并发模型同时包含异步(能挂起让出线程)和并行(多核同时跑多段代码)。日常我们用”并发”统称这两者。它的杀手锏是:Swift 在编译期就能拦住大部分数据竞争(data race,多段代码同时改同一份数据),很多隐患直接变成编译错误。

1-2 用 async 定义异步函数

定义一个异步函数,在参数后面加 async 关键字,位置跟标 throws 很像。如果函数有返回值,async 写在返回箭头 -> 前面:

func listPhotos(inGallery name: String) async -> [String] {
    // 这里假设在做网络请求
    return ["照片1", "照片2", "照片3"]
}

如果同时会抛错,写 async throws,顺序上 async 在前 throws 在后:

func listPhotos(inGallery name: String) async throws -> [String] {
    try await Task.sleep(for: .seconds(2))   // 模拟等待 2 秒
    return ["照片1", "照片2", "照片3"]
}

Task.sleep(for:) 是学习并发时最有用的小工具:它把当前任务挂起至少指定时长,常用来模拟网络延迟。它本身就是异步且可能抛错的,所以前面要写 try await

1-3 用 await 调用

调用异步函数时,在前面写 await,标记”这里可能挂起”。这点跟调用抛错函数写 try 一样,都是为了提醒你:程序流向在这里可能变化。

func showGallery() async {
    let names = await listPhotos(inGallery: "暑假旅行")
    let first = names.sorted()[0]
    let photo = await downloadPhoto(named: first)
    print(photo)
}

注意两个 await:第一次等照片列表,第二次等单张照片下载。中间的 sorted()[0] 是普通同步代码,没有 await,不会挂起。

Note

挂起是合作式的:代码只会在你写了 await 的调用点才可能被挂起,绝不会在毫无征兆的地方突然停。每一个潜在的暂停点都被 await 标得一清二楚,这让并发代码读起来心里有底。

1-4 挂起时发生了什么

当你在一个 await 处挂起,Swift 把当前这段代码在所在线程上”暂停”,转去跑别的并发代码。等异步函数返回,代码再恢复。恢复后不保证跑在原来的线程上——你不用关心这个,Swift 替你管。

正因为能挂起,只有少数地方能调用异步函数:

  • 另一个异步函数、方法或属性的体内。
  • @main 标记的类型的静态 main() 方法里。
  • 非结构化子任务(Task {} 块)里。

换句话说,异步代码必须活在异步上下文里。这是强制的,编译不过就说明你上下文不对。

1-5 同步代码不能直接调异步

这是初学者最容易撞墙的一点:你没法把异步代码包一层就变成同步、然后同步等结果。Swift 标准库故意不提供这种”在同步里等异步”的能力,因为那会引入微妙的竞争、线程问题甚至死锁。

// 错误思路:下面这种"同步调异步并等待"是不允许的
func bad() -> [String] {
    // return await listPhotos(...)  // 编译错误:同步函数里不能写 await
}

正确的做法是从上往下改:先把最外层的代码改成异步(比如 main 或某个异步函数),再逐层把它调用的函数也改成 async。这是个”自顶向下”的迁移过程,没法自底向上。

Tip

如果你之前用过回调闭包(completion handler)那套写法,会记得层层嵌套的”金字塔噩梦”。async/await 最大的好处,就是把这些嵌套拍平成一串顺眼的线性代码,读起来跟同步几乎一样。

1-6 一个能跑的最小示例

在脚本或 Playground 里,把你调用异步函数的代码放进 Task {} 块,它就进入了异步上下文:

import Foundation

func greet(name: String) async -> String {
    try? await Task.sleep(for: .seconds(1))
    return "你好,\(name)!"
}

Task {
    let message = await greet(name: "小明")
    print(message)   // 约 1 秒后打印:你好,小明!
}

Task {} 开启一个并发任务,里面可以用 await。把需要演示的异步调用都放进去,就构成了一个最小的并发示例。greetTask.sleep 模拟延迟,你能直观感到”挂起等待”的存在。

Warning

异步函数里如果同时标了 throws,调用时 tryawait 都要写,顺序无所谓(try awaitawait try 都行)。忘写 try 是最常见的编译报错,看到”call can throw”就补 try

1-7 异步函数与抛出函数的相似与不同

异步函数和抛出函数很像:定义时一个标 async、一个标 throws;调用时一个写 await、一个写 try。异步函数也能调异步函数,就像抛错函数能调抛错函数。

但它们有个本质不同。抛错代码你能用 do-catch 包起来,从而在同步代码里调用抛错函数——错误被就地处理了。异步代码没有这种”就地包裹”的等价物。正因如此,并发必须自顶向下渗透,这是 Swift 刻意的取舍,不是疏忽。

1-8 一个常见困惑:挂起等于让出线程吗

初学者常把”挂起”理解成”线程被占着干等”,其实正好相反。代码在 await 处挂起时,它让出了当前线程,线程立刻去跑别的并发代码。等异步结果就绪,这段代码再被唤醒、可能在另一个线程上继续。

这正是 async/await 比老式回调高效的地方:等待期间线程不空转、不阻塞,被充分复用。你写的 await 看起来像”停在这里等”,底层却是”放手让线程去忙别的”。这个认知对理解并发性能很关键——挂起不是浪费,而是把等待时间还给系统。

Note

你不需要(也无法)手动指定”这段代码跑在哪个线程”。Swift 的并发运行时负责调度。你只声明”这里可以挂起”,剩下的交给它。这种”不碰线程”的设计,是 Swift 并发相比裸线程/锁更安全的根源之一。

1-9 小结与本模块地图

async 声明异步函数(可挂起),await 标记调用处的潜在挂起点。挂起会让出线程、恢复后继续,整个过程由 Swift 管理,你不必碰线程。同步代码无法直接等待异步结果,必须自顶向下采用并发。

接下来几章会沿着这条线展开:第 81 章讲怎么用 async let 和任务组把多个异步调用并行跑起来;第 82 章深入 Task 与取消;第 83 章用 actor 保护共享数据;第 84 章讲跨并发域传递数据的 Sendable;第 85 章讲主线程与 @MainActor;第 86 章讲异步序列;第 87 章总结陷阱与最佳实践。