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)")
}
执行流程是这样的:
process()开始执行,遇到await step1()- 程序暂停
process()的执行,转而执行step1() step1()内部遇到await Task.sleep,再次暂停- 此时线程可以去做其他事情(比如处理 UI 事件)
- 1秒后,
step1()恢复执行,返回结果 process()拿到结果,继续执行到await step2()- 重复上述暂停-恢复流程
说白了,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 和结构化并发,到时候你会发现,今天这些基础就像搭积木的底座——底座稳了,上面才能盖高楼。
记住:写异步代码,心里要时刻想着「这里会不会暂停?」、「暂停后线程去干嘛了?」。保持这个意识,你很快就能写出健壮的并发代码。