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 写后端也会更顺手

相关推荐
HarderCoder1 天前
深入理解 SwiftUI 中的 `@Observable` 与 `@Bindable`:从原理到实践
swiftui·swift
00后程序员张2 天前
iOS 26 兼容测试实战,机型兼容、SwiftUI 兼容性改动
android·ios·小程序·uni-app·swiftui·cocoa·iphone
大熊猫侯佩3 天前
雪山飞狐之 Swift 6.2 并发秘典:@concurrent 的江湖往事
swiftui·swift·apple
胎粉仔5 天前
Objective-C —— APIs declaration 自定义
ios·objective-c·swift
用户095 天前
Swift Concurrency 中的 Threads 与 Tasks
ios·swiftui·swift
低调小一5 天前
双端 FPS 全景解析:Android 与 iOS 的渲染机制、监控与优化
android·ios·kotlin·swift·fps
用户095 天前
更现代、更安全:Swift Synchronization 框架与 Mutex 锁
ios·面试·swift
大熊猫侯佩8 天前
鹿鼎记豪侠传:Rust 重塑 iOS 江湖(下)
rust·objective-c·swift