Swift 结构化并发 async let

FreeGuideOnline 最新 2026-07-12

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 letTaskGroup 结合,但 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 letTask 的区别

初学者容易混淆两者,简单区分:

特性 async let Task (非结构化)
生命周期 绑定到当前作用域,作用域结束自动取消 独立于当前作用域,需要手动管理取消
结构化 结构化并发,错误/取消自动传播 非结构化,错误不会自动传播到父任务
用途 简单的并行任务,数量固定 需要独立生命周期、优先级控制、后台任务等
代码量 极简 相对较多

因此,在大多数“并行加载多个数据”的场景下,优先使用 async let 是明智之选。


总结

async let 是 Swift 结构化并发中一颗小巧精致的珍珠。它用最自然的方式让多个异步操作并行执行,同时保留了结构化并发的安全性——自动取消、错误传播和作用域管理。掌握它,你的异步代码将变得更加高效、清晰且富有表现力。

现在,试着在你的代码中找出那些本可以并行却顺序执行的 await,用 async let 给它一次漂亮的重构吧。