11-15.【错误处理】async throws 与 throws 的底层调用机制差异是什么?

2 阅读3分钟

从底层机制上看,throwsasync throws 虽然在语法上只有一字之差,但它们的控制流模型寄存器使用以及状态机转换有着本质的区别。

我们可以从以下三个维度来拆解它们的底层差异:


1. 堆栈模型与控制流 (Stack vs. Continuation)

同步 throws:基于线性跳转

同步函数的调用遵循传统的 栈帧(Stack Frame) 模型。

  • 机制:当 throw 发生时,函数会执行栈清理,然后直接通过 ret 指令跳转回调用方的地址。
  • 控制流:它是一条直线。报错后的跳转开销极低,因为它不涉及线程切换或上下文保存。

异步 async throws:基于状态机 (State Machine)

异步函数在编译后会被拆分为多个片段(Partial Functions)。

  • 机制:每一个 await 都是一个潜在的挂起点。当 async throws 报错时,它不仅要传递错误,还要触发异步上下文(Continuation)的恢复
  • 控制流:它是断续的。错误发生后,运行时必须决定将控制权交回给哪一个调度器(Executor),并恢复调用方的执行上下文。

2. 寄存器分配与 ABI 差异

在 Swift 的 ABI(应用二进制接口) 层面,两者对硬件寄存器的利用方式不同:

同步 throws (The Error Register)

  • x21 (ARM64) :专门预留给错误指针。
  • 逻辑:函数返回后,CPU 立即检查 x21。如果 x21 != 0,则代表有异常,直接进入 catch 逻辑。

异步 async throws (The Async Context)

  • x22 (ARM64) :通常用于存放 Async Context 指针。
  • 逻辑:由于异步函数可能在不同的线程恢复,错误信息不能仅靠一个物理寄存器长久持有。错误通常被写入到异步任务的 任务局部存储(Task Local Storage)Continuation 结构体 中。
  • 差异点async throws 在抛出时,需要将错误对象从当前的执行上下文“封送(Marshaling)”到调用者的恢复上下文中。

3. 错误传播的性能成本 (Cost Analysis)

维度throws (同步)async throws (异步)
正常路径开销接近零(仅多一个寄存器清零)中等(需维护异步栈和状态机)
错误触发开销(本地跳转 + 寄存器赋值)(涉及任务恢复 + 调度器排队)
清理机制简单的栈清理(Pop Frame)复杂的延续清理(Resume Continuation)

4. 防御式设计的关键点

基于这些底层差异,我们在设计架构时应注意:

  1. 避免在 async throws 中进行高频、微小的报错

    同步 throws 的开销很小,可以当做逻辑分支使用。但 async throws 的报错涉及异步环境的恢复,如果在一个每秒循环数万次的异步流(AsyncSequence)中频繁抛出错误,性能会急剧下降。

  2. 利用局部同步化降低成本

    如果一个复杂的异步逻辑内部有许多细碎的可能报错点,建议将这些逻辑封装在一个同步throws 子函数中,由异步函数统一调用一次。

    Swift

    // ✅ 推荐做法:将频繁报错的逻辑同步化,减少异步状态机切换
    func processData() async throws {
        let rawData = try await download()
        try self.syncValidator(rawData) // 同步抛出,成本极低
    }
    
  3. 任务取消(Cancellation)的特殊性

    async throws 有一个独特的底层机制:它会定期检查任务的取消标志位。如果任务已取消,它会“伪装”成一个错误抛出。这在底层是由异步运行时自动插入的检查指令完成的。


总结

  • throws指令级的跳转,它利用物理寄存器实现极速的错误传递。
  • async throws任务级的跳转,它利用堆内存中的状态机实现跨线程、跨时间的错误恢复。