一句话结论
async函数在挂起时不会保留传统的调用栈;
它把“未来继续执行所需的一切”保存到一个 async context(也叫 coroutine frame / task frame) 中,
调用栈在await处被完全展开(unwind) ,恢复时通过 continuation + resume function 重新“接上”。
1️⃣ 同步调用栈 vs async 调用栈
同步函数
main
└─ foo
└─ bar
└─ baz
- 栈帧连续
- 返回地址由 CPU 保存
- 局部变量在栈上
async 函数(遇到 await)
main
└─ foo (async)
└─ await bar()
⛔ 栈在这里被拆掉
关键点:
await不是普通函数调用,而是一个可中断点
2️⃣ async 函数真正保存的是什么?
当执行到 await 时,Swift 会:
✅ 保存到 async context(堆分配)
这个 async context 本质是一个结构体,包含:
AsyncContext {
resume_function_ptr // 恢复执行的入口
state // 当前状态机位置
local_variables // 跨 await 存活的局部变量
parent_context // 调用者的 async context
executor // 当前 executor
}
你可以把它理解成:
“一个被堆化的栈帧 + 程序计数器 + continuation”
3️⃣ await 发生时的真实流程(逐步)
以这个代码为例:
func foo() async {
let x = await bar()
print(x)
}
① 执行到 await
-
编译器插入:
swift_task_suspend
-
当前函数:
- 把
x的 storage 预留在 async context - 设置
state = 1 - 记录 resume function
- 把
② 调用栈被展开(unwind)
foo async frame ← 保存到堆
↑
bar async frame ← 由 runtime 接管
- 没有任何栈帧停留
- 线程可以立刻被复用
- 这是 Swift async 高性能的关键之一
③ 恢复(resume)
当 bar() 完成:
- runtime 找到 continuation
- 调用
resume_function(context) - 执行类似:
switch context.state {
case 1:
let x = context.x
print(x)
}
4️⃣ async 调用链是怎么“接起来”的?
async context 是一个链表
Task
└─ foo context
└─ bar context
└─ baz context
但注意:
-
不是物理栈
-
是通过
parent_context指针串起来 -
用于:
- 错误传播
- cancellation
- task-local
- backtrace(部分)
5️⃣ 为什么不能保存真实调用栈?
三个原因:
🚫 1. 线程不固定
await foo()
恢复时可能在 完全不同的线程。
🚫 2. 栈空间不可迁移
-
原线程的栈:
- 可能已被复用
- 甚至已释放
-
保存整个栈:
- 成本高
- 不可移植
🚫 3. 性能与可组合性
Swift 选择:
堆化最小必要状态,而不是整个栈
6️⃣ 那“调用栈”调试怎么办?
Swift 目前做的是:
-
逻辑 async stack trace
-
通过:
- async context 链
- resume function 的 metadata
-
在 LLDB / 崩溃日志中拼出来
但它:
- 不是 100% 的物理栈
- 是“语义栈”
7️⃣ 和 C++ / Go 的对比(重要)
| 语言 | async 上下文 |
|---|---|
| Swift | 显式 async frame + 状态机 |
| C++20 | coroutine frame(非常相似) |
| Go | 分段栈 + goroutine |
| JS | Promise + continuation |
Swift 不是 goroutine,也不保存栈。
8️⃣ 核心记忆点(超重要)
Swift async 函数的“调用栈”不是栈,而是一串 async context
await 时真正保存的是:
👉 程序计数器 + 必要局部变量 + continuation + executor