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

相关推荐
非专业程序员Ping3 小时前
一文读懂字符、字形、字体
ios·swift·font
东坡肘子8 小时前
去 Apple Store 修手机 | 肘子的 Swift 周报 #0107
swiftui·swift·apple
非专业程序员1 天前
iOS/Swift:深入理解iOS CoreText API
ios·swift
xingxing_F1 天前
Swift Publisher for Mac 版面设计和编辑工具
开发语言·macos·swift
YGGP2 天前
【Swift】LeetCode 438. 找到字符串中所有字母异位词
swift
QWQ___qwq3 天前
Swift中.gesture的用法
服务器·microsoft·swift
QWQ___qwq3 天前
SwiftUI 布局之美:Padding 让界面呼吸感拉满
ios·swiftui·swift
用户093 天前
SwiftUI 键盘快捷键作用域深度解析
ios·面试·swiftui
用户093 天前
Xcode 26 的10个新特性解析
ios·面试·swift
白熊1884 天前
【图像大模型】ms-swift 深度解析:一站式多模态大模型微调与部署框架的全流程使用指南
开发语言·ios·swift