从底层机制上看,throws 和 async 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. 防御式设计的关键点
基于这些底层差异,我们在设计架构时应注意:
-
避免在
async throws中进行高频、微小的报错:同步
throws的开销很小,可以当做逻辑分支使用。但async throws的报错涉及异步环境的恢复,如果在一个每秒循环数万次的异步流(AsyncSequence)中频繁抛出错误,性能会急剧下降。 -
利用局部同步化降低成本:
如果一个复杂的异步逻辑内部有许多细碎的可能报错点,建议将这些逻辑封装在一个同步的
throws子函数中,由异步函数统一调用一次。Swift
// ✅ 推荐做法:将频繁报错的逻辑同步化,减少异步状态机切换 func processData() async throws { let rawData = try await download() try self.syncValidator(rawData) // 同步抛出,成本极低 } -
任务取消(Cancellation)的特殊性:
async throws有一个独特的底层机制:它会定期检查任务的取消标志位。如果任务已取消,它会“伪装”成一个错误抛出。这在底层是由异步运行时自动插入的检查指令完成的。
总结
throws是指令级的跳转,它利用物理寄存器实现极速的错误传递。async throws是任务级的跳转,它利用堆内存中的状态机实现跨线程、跨时间的错误恢复。