async let 也能调度同步函数?——Swift 并发隐藏小技巧详解

什么是 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
  • 期间主线程完全不被卡住(可在界面上拖拽按钮验证)

原理剖析:为什么能这样写?

  1. async let 右侧的表达式会被 隐式包装成 Task {}
  2. Taskoperation 闭包会在 全局并发线程池 上调度
  3. 因为闭包与调用上下文脱离,所以即使内部是「同步」代码,也不会阻塞原线程
  4. 返回值通过 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 队列,逻辑自上而下

我的观点

  1. async let 的「同步函数调度」能力,让老项目渐进迁移到 Swift 并发变得异常丝滑:

    旧模块不改代码,就能被新并发代码调用,先享受「并行」红利,再逐步把函数标成 async

  2. 它本质上仍是「Task 语法糖」,所以不要滥用:

    • 长时运算应检查取消
    • 大量任务请用 TaskGroup 限制并发度
  3. 结合 actor / MainActor 可以做「线程安全+并行」的优雅架构;未来再配合 distributed actor 写后端也会更顺手

相关推荐
HarderCoder4 小时前
Swift 一个小型游戏对象模型渐进式设计(四)——类型擦除与 Existential:当泛型遇见动态派发
swift
HarderCoder4 小时前
Swift 一个小型游戏对象模型渐进式设计(五)——Swift 并发世界:把 Attackable 搬进 actor
swift
HarderCoder4 小时前
Swift 一个小型游戏对象模型渐进式设计(三)——把能力再抽象一层,写一套“伤害计算器”框架
swift
HarderCoder4 小时前
Swift 一个小型游戏对象模型渐进式设计(二)——协议与默认实现:如何写出不用继承的多态
swift
HarderCoder4 小时前
Swift 一个小型游戏对象模型渐进式设计(一)——继承机制解读:从基础类到防止重写
swift
HarderCoder6 小时前
Swift 中的迭代机制:Sequence、Collection 与 Iterator 完全拆解
swift
HarderCoder11 小时前
告别并发警告:Swift 6 线程安全通知 MainActorMessage & AsyncMessage 实战指南
swift
HarderCoder11 小时前
【SwiftUI 任务身份】task(id:) 如何正确响应依赖变化
swift
非专业程序员12 小时前
精读GitHub - swift-markdown-ui
ios·swiftui·swift
5***79001 天前
Swift进阶
开发语言·ios·swift