Swift 结构化并发 async let
Swift 结构化并发:掌握 async let 让任务并行处理
在 Swift 5.5 引入的结构化并发模型中,async let 是一个极为简洁优雅的语法。它允许你在编写异步代码时,像声明普通常量那样声明一个异步值,并让多个独立的异步操作自动并行执行。本文将通过清晰的示例,带你从零开始理解并掌握 async let 的使用方式、运行原理以及注意事项。
什么是 async let
简单来说,async let 是用来声明一个异步常量的。当 Swift 遇到 async let 时,会立即启动一个新的子任务去执行右侧的异步函数,但不会等待该函数完成。你可以继续执行后续代码。直到你真正使用那个常量时,程序才会挂起并等待结果返回。
这完美体现了“即声明,即启动,即访问,即等待”的模式。
为什么要用 async let —— 从顺序执行到并行执行
假设我们有两个完全独立、不互相依赖的耗时异步函数:
func fetchUserName() async throws -> String {
// 模拟网络请求
try await Task.sleep(nanoseconds: 1_000_000_000)
return "Antoine"
}
func fetchFriendCount() async throws -> Int {
try await Task.sleep(nanoseconds: 1_500_000_000)
return 42
}
传统的顺序 await 写法:
func loadProfile() async throws {
let name = try await fetchUserName() // 等待 1 秒
let count = try await fetchFriendCount() // 再等待 1.5 秒
print("\(name) has \(count) friends")
}
// 总耗时 ≈ 2.5 秒
两个函数互不依赖,却被迫一个接一个执行,白白浪费了等待时间。
使用 async let 并行执行:
func loadProfile() async throws {
async let name = fetchUserName() // 立即启动子任务,不等待
async let count = fetchFriendCount() // 立即启动另一个子任务,不等待
// 当第一次使用 name 和 count 时,同时等待它们完成
let (userName, friendCount) = try await (name, count)
print("\(userName) has \(friendCount) friends")
}
// 总耗时 ≈ max(1秒, 1.5秒) = 1.5 秒
在这个例子中,两个异步调用几乎同时开始,当我们需要结果时使用元组解构来等待,Swift 会自动协调等待它们全部完成。代码结构清晰,性能成倍提升。
async let 的语法细节与约束
声明与绑定
async let 本质上创建了一个子任务,该子任务的生命周期被限制在当前作用域内。因此你必须在作用域结束前使用该常量,否则任务会被隐式取消。
func example() async throws {
async let greeting = "Hello, world!" // ✅ 即便右侧不是异步函数也可以,但通常用于异步
print(await greeting)
}
必须被 await 标记使用
所有通过 async let 声明的常量,在取值时必须使用 await 关键字,以表明此处可能会挂起等待。
不能使用 var
async let 的值一旦绑定,就不能修改。它强调不可变性,与结构化并发的“结构”保持一致。
只能在异步上下文中使用
毫无疑问,使用 async let 的函数本身必须是 async 标记的,或者在 Task 闭包内。
错误处理与取消传播
结构化并发的一个重要特性就是错误会自动传播。如果任何一个 async let 子任务抛出错误,当你尝试 await 那个值时,错误会被抛出。更关键的是,如果其中一个子任务抛出,其他尚未完成的兄弟子任务会被自动标记为取消,以节省系统资源。
func throwsError() async throws -> String {
try await Task.sleep(nanoseconds: 500_000_000)
throw NSError(domain: "Test", code: 1)
}
func normalTask() async throws -> Int {
try await Task.sleep(nanoseconds: 1_000_000_000)
return 100
}
func runTasks() async throws {
async let stringValue = throwsError()
async let intValue = normalTask()
// 这里会抛出错误,同时 intValue 对应的子任务会被取消
let _ = try await (stringValue, intValue)
}
注意: 如果你需要手动处理取消,可以在异步函数内部使用
Task.checkCancellation()或Task.isCancelled。
一个实际场景:同时获取多个 API 数据
很多时候我们需要渲染一个页面,该页面依赖来自几个不同接口的数据,例如用户信息、订单列表、推荐商品。它们之间不存在先后依赖,正是 async let 的用武之地。
func loadDashboard() async throws -> Dashboard {
async let user = fetchUser()
async let orders = fetchRecentOrders()
async let recommendations = fetchRecommendations()
// 这三个请求同时发出,极大缩短页面加载时间
return try await Dashboard(user: user, orders: orders, recommendations: recommendations)
}
你甚至可以将 async let 与 TaskGroup 结合,但 async let 的优势在于它书写极其简单,适用于已知并发数量的场景。
常见误区与最佳实践
❌ 误区一:忘记使用 await
async let name = fetchUserName()
print(name) // 错误!必须使用 await
❌ 误区二:在 async let 后面立即 await
async let name = fetchUserName()
let userName = await name // 此时只启动了一个任务,相当于顺序执行
async let count = fetchFriendCount()
这样写虽然语法正确,但没有充分利用并行性。最佳实践是先声明所有 async let,再集中等待。
❌ 误区三:用 async let 替代依赖链
如果第二个任务依赖第一个任务的结果,那就不能使用 async let 并行。此时只能用顺序 await。
// 错误示范:
async let token = fetchToken()
async let profile = fetchProfile(withToken: await token) // 这里其实已经是顺序了
正确的做法是直接顺序写,或调整 API 设计。
✅ 最佳实践总结
- 用于执行彼此独立的异步操作。
- 声明完成后集中
await。 - 留意作用域,避免
async let值在未使用前作用域结束(编译器会警告)。 - 结合元组解构或
let绑定一次性获取所有结果。 - 当并发数量不确定或动态时,改用
TaskGroup。
async let 与 Task 的区别
初学者容易混淆两者,简单区分:
| 特性 | async let |
Task (非结构化) |
|---|---|---|
| 生命周期 | 绑定到当前作用域,作用域结束自动取消 | 独立于当前作用域,需要手动管理取消 |
| 结构化 | 结构化并发,错误/取消自动传播 | 非结构化,错误不会自动传播到父任务 |
| 用途 | 简单的并行任务,数量固定 | 需要独立生命周期、优先级控制、后台任务等 |
| 代码量 | 极简 | 相对较多 |
因此,在大多数“并行加载多个数据”的场景下,优先使用 async let 是明智之选。
总结
async let 是 Swift 结构化并发中一颗小巧精致的珍珠。它用最自然的方式让多个异步操作并行执行,同时保留了结构化并发的安全性——自动取消、错误传播和作用域管理。掌握它,你的异步代码将变得更加高效、清晰且富有表现力。
现在,试着在你的代码中找出那些本可以并行却顺序执行的 await,用 async let 给它一次漂亮的重构吧。