2-2.【Concurrency】async 函数在调用栈中如何保存执行上下文?

4 阅读2分钟

一句话结论

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++20coroutine frame(非常相似)
Go分段栈 + goroutine
JSPromise + continuation

Swift 不是 goroutine,也不保存栈。


8️⃣ 核心记忆点(超重要)

Swift async 函数的“调用栈”不是栈,而是一串 async context
await 时真正保存的是:
👉 程序计数器 + 必要局部变量 + continuation + executor