什么是 async let
async let
是 Swift 5.5 引入的「结构化并发」语法糖之一- 它允许你把「多个异步操作」并行地扔给后台,然后在需要结果时用
await
一次性收回来 - 写起来比
TaskGroup
简洁,比「回调金字塔」优雅
最小可运行模板:
swift
// 异步函数示例
func fetchA() async -> Int {
try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 s
return 1
}
func fetchB() async -> Int {
try? await Task.sleep(nanoseconds: 300_000_000) // 0.3 s
return 2
}
// 并发调用
func demo() async {
async let a = fetchA() // 立即开始,不阻塞
async let b = fetchB() // 同上
let sum = await a + b // 这里才阻塞,直到两者都完成
print("sum = \(sum)") // 3
}
隐藏特性:async let 也能接「同步」函数
大多数教程只告诉你:"右侧必须是一个 async 返回的表达式"
但真相是:只要右侧表达式最终能产生一个 async
值,编译器就放行。
最简单的办法:把「同步函数调用」直接塞进 async let
。
swift
// 1️⃣ 一个再普通不过的同步函数
func heavySyncJob(id: Int) -> Int {
// 故意耗时
for _ in 0..<1_000_000 { _ = id & 1 }
return id * 2
}
// 2️⃣ 以前我们要么
// - 把 heavySyncJob 改成 async
// - 或者在 Task { ... } 里手动 .await
//
// 3️⃣ 现在一行代码就能把它扔后台:
func runConcurrent() async {
async let x = heavySyncJob(id: 1) // ✅ 编译通过!
async let y = heavySyncJob(id: 2) // ✅ 同样有效
let result = await x + y // 3 000 000 次空转后返回
print("result = \(result)")
}
运行现象:
- Xcode 控制台先空白几秒,然后一次性打印
result = 6
- 期间主线程完全不被卡住(可在界面上拖拽按钮验证)
原理剖析:为什么能这样写?
async let
右侧的表达式会被 隐式包装成Task {}
- 该
Task
的operation
闭包会在 全局并发线程池 上调度 - 因为闭包与调用上下文脱离,所以即使内部是「同步」代码,也不会阻塞原线程
- 返回值通过
Task.result
转回async let
所生成的Future
,最终由await
解开
用伪代码还原编译器行为:
swift
// 你写的
async let x = heavySyncJob(id: 1)
// 编译器大概帮你展开成
let __task_x = Task { heavySyncJob(id: 1) } // 扔到后台
let x = await __task_x.value // 需要时等待
注意:这属于「语法糖」而非魔法,性能与你自己手写 Task {}
几乎一致;但代码量更少、结构化更强。
别忘了错误处理
如果同步函数会 throws
,同样适用:
swift
enum SomeError: Error { case zero }
func riskySync(_ n: Int) throws -> Int {
guard n != 0 else { throw SomeError.zero }
return n * 10
}
func testThrows() async {
async let a = riskySync(5) // 不会抛在此处
async let b = riskySync(0) // 同样不会立即抛
do {
let sum = try await a + b // ❗️b 会在这里抛
print("sum = \(sum)")
} catch {
print("捕获到错误:\(error)")
}
}
实战建议与踩坑提醒
场景 | 建议 |
---|---|
大量 CPU 密集任务 | 用 async let 可以并行,但别一次性开几百个;控制并发量可用 TaskGroup 或信号量 |
需要取消 | async let 会随作用域退出自动取消,但同步函数本身「不感知」取消。需要手动检查 Task.isCancelled 并提前返回 |
访问主线程资源 | 同步函数里若操作 UI / CoreData 主队列对象,要先 await MainActor.run 切回来 |
单元测试 | 在 @MainActor 测试方法里,async let 依旧会把闭包放到后台,注意线程断言 |
小结
async let
并非只能绑定async
函数;任何表达式只要能最终产出async
值就能用- 把「同步函数」直接塞进
async let
,编译器会隐式生成Task
并调度到全局线程池 - 写法极简、结构化、易读,是「一行代码后台并行」的利器
- 但仍要关注「取消、错误、线程安全」等传统并发问题
扩展场景:批量压缩图片
假设 App 需要把 10 张原图同步压缩后上传,压缩算法是第三方库,只提供同步 API:
swift
import UIKit
/// 第三方仅提供同步压缩
func compressSync(_ image: UIImage, width: CGFloat) -> Data {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: width, height: width))
let resized = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: CGSize(width: width, height: width)))
}
return resized.jpegData(compressionQuality: 0.8)!
}
func upload(_ data: Data) async throws -> String {
// 伪代码:真正上传
try await Task.sleep(nanoseconds: 300_000_000)
return "https://example.com/\(data.count)"
}
func compressAndUpload(images: [UIImage]) async throws -> [String] {
// 1. 批量压缩(并行)
var compressed: [Data] = []
for img in images {
async let data = compressSync(img, width: 1024) // 同步函数直接扔后台
compressed.append(try await data)
}
// 2. 批量上传(继续并行)
var urls: [String] = []
for data in compressed {
async let url = upload(data)
urls.append(try await url)
}
return urls
}
优势:
- 压缩阶段全部并行,CPU 打满;上传阶段同样并行,网络打满
- 主线程全程无阻塞,UI 可展示进度条
- 代码保持「结构化」------没有回调,没有 GCD 队列,逻辑自上而下
我的观点
-
async let
的「同步函数调度」能力,让老项目渐进迁移到 Swift 并发变得异常丝滑:旧模块不改代码,就能被新并发代码调用,先享受「并行」红利,再逐步把函数标成
async
-
它本质上仍是「Task 语法糖」,所以不要滥用:
- 长时运算应检查取消
- 大量任务请用
TaskGroup
限制并发度
-
结合
actor
/MainActor
可以做「线程安全+并行」的优雅架构;未来再配合distributed actor
写后端也会更顺手