异步的本质:顺序与执行的拆分
通过个人理解,可以把异步问题拆成两部分:
- 顺序:如何描述“接下来做什么”
- 执行:谁来在合适的时机触发这些步骤
同步代码中,这两件事是绑定在一起的:
auto data = downloadData();
updatePage(data);
顺序写在代码里,执行由调用栈立即完成。而异步的核心变化在于:
顺序和执行被拆开了。
Callback:把“顺序”从调用栈中拿出来
目标
Callback 模型试图解决的问题很直接:
- 发起操作时不阻塞线程
- 在等待 IO 的过程中,程序仍然可以继续执行
基本形式
void downloadData(std::function<void(std::string)> callback) {
std::thread([callback]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::string data = "服务器返回的数据";
callback(data);
}).detach();
}
调用方式:
downloadData([](std::string data) {
updatePage(data);
});
这里发生了一个关键变化:
“下一步做什么”不再依赖返回值,而是被作为参数传入。
顺序的表达方式改变了
同步代码中:
auto data = downloadData();
updatePage(data);
顺序通过返回值串联。
而在 callback 中:
downloadData([](std::string data) {
updatePage(data);
});
顺序通过 callback 表达。
也就是说:
- 同步:顺序在调用栈里
- 异步:顺序被显式传递
本质:continuation 被显式化
callback 实际上就是 continuation,也就是“接下来要执行的代码”。
同步:
auto data = downloadData();
// continuation 在调用栈中
updatePage(data);
异步:
downloadData([](std::string data) {
// continuation 被手动包装
updatePage(data);
});
这一步的意义在于:
程序不再依赖调用栈来保存执行状态,而是由开发者自己管理 continuation。
Callback 的问题:顺序被结构吞掉
当只有一步时,callback 很自然。
但当依赖变多时:
login([](User user) {
getProfile(user, [](Profile p) {
getPosts(p.id, [](Posts posts) {
render(posts);
});
});
});
原本是线性的顺序:
login → getProfile → getPosts → render
却变成了嵌套结构。
问题不在于“难看”,而在于:
顺序被编码进了语法结构本身。
于是:
- 控制流不再是线性的
- continuation 被层层嵌套
- 无法独立操作和组合
这就是 callback 的极限。
Callback 容器:从“嵌套”到“PUSH”
为了解决这个问题,一个自然的想法出现了:
不要立即执行 callback,而是先把它们收集起来。
也就是说,把 callback 从“语法里的下一步”变成“容器中的数据”。
一个实际的实现
template<typename T = void>
class AbilityTransactionCallbackInfo {
public:
using CallbackFunc = std::function<void(T&)>;
static AbilityTransactionCallbackInfo *Create()
{
return new(std::nothrow) AbilityTransactionCallbackInfo();
}
static void Destroy(AbilityTransactionCallbackInfo *callbackInfo, void* data = nullptr)
{
callbackInfo->Finalize(data);
delete callbackInfo;
}
void Push(const CallbackFunc &callback)
{
callbackStack_.push(callback);
}
void SetFinalizeCallback(const std::function<void(void*)> &finalize)
{
finalizeCallback_ = finalize;
}
void Finalize(void* data)
{
if (finalizeCallback_ != nullptr) {
finalizeCallback_(data);
finalizeCallback_ = nullptr;
}
}
void Call(T &callbackResult)
{
while (!callbackStack_.empty()) {
CallbackFunc &callbackFunc = callbackStack_.top();
callbackFunc(callbackResult);
callbackStack_.pop();
}
}
private:
std::stack<CallbackFunc> callbackStack_;
std::function<void(void*)> finalizeCallback_;
};
核心变化:callback 被“压入”,而不是“嵌套”
使用方式不再是:
downloadData([](data) {
step1(data);
step2(data);
});
而是:
auto* info = AbilityTransactionCallbackInfo<Result>::Create();
info->Push(step1);
info->Push(step2);
这里的变化非常关键:
callback 不再写在语法结构里,而是被压入一个容器。
顺序开始由“数据结构”决定,而不是“代码嵌套”。
Push 与 Call:注册与触发分离
这个模型可以明显分成两个阶段:
第一阶段是注册:
info->Push(callback);
只是记录未来要执行的逻辑。
第二阶段是触发:
info->Call(result);
统一执行所有 callback。
这一步把 callback 的语义从“立即调用”变成了“延迟执行”。
Finalize:异步不只有“结果”
这个类里还有一个额外的机制:
finalizeCallback_
它代表的是:
整个异步流程结束之后的收尾逻辑。
例如:
- 资源释放
- 生命周期结束
- 上下文清理
这说明异步流程不仅仅是“执行 callback”,还包括“结束后的处理”。
Runtime:谁来触发 Call
到这里为止,我们已经解决了:顺序如何被组织(callback → 容器 → PUSH)
但还有一个问题:什么时候调用 Call?
执行者的角色
在最初的例子中,是线程:
std::thread(...).detach();
在更通用的模型中,是 runtime:
- event loop
- 线程池
- IO 调度器
它们的职责是:
在某个时刻触发:
info->Call(result);
一个抽象模型
runtime 可以理解为:
while (true) {
auto task = queue.pop();
task();
}
它做的事情是:
- 等待事件完成
- 把 callback 放入队列
- 在循环中执行
到这里的完整模型
现在可以把系统拆成两部分:
- callback / 容器:定义顺序
- runtime:负责执行
顺序与执行完全解耦。
Promise:结构化的 callback 容器
callback 容器已经解决了一个问题:
顺序可以被收集,而不是嵌套。
但它仍然缺少一个关键能力:
状态。
例如:
- 任务是否完成
- 是否失败
- 如果 callback 在完成之后才注册怎么办
Promise 做的事情
Promise 在 callback 容器之上增加了:
- 状态(pending / fulfilled / rejected)
- 回调分类(then / catch)
- 与 runtime 的标准化对接
顺序重新变成链
login()
.then(getProfile)
.then(getPosts)
.then(render);
这里的顺序不再是嵌套,而是链式结构。
本质上仍然是:
- 保存 continuation
- 逐步触发
只是形式从“栈”变成了“链”。
async / await:把链重新铺平成代码
再往上一步,就是 async / await:
auto data = await downloadData();
process(data);
看起来像同步代码,但本质没有变化。
底层仍然是:
- continuation 被保存
- runtime 在未来恢复执行
只是:
- Promise 帮你管理容器
- 编译器帮你生成 continuation
最终统一
从 callback 到 async/await,可以统一成同一个模型:
- 最开始:callback 手动表达 continuation
- 然后:callback 被放入容器(PUSH)
- 接着:Promise 标准化容器 + 状态
- 最后:async/await 把 continuation 隐藏在语法中
整个过程中唯一不变的是:
continuation 被保存,并由 runtime 在未来某个时刻执行。
概述
异步的本质不是“多线程”,也不是“语法技巧”,而是两件事:顺序如何被表达。执行如何被调度。
callback 解决了顺序的表达,但带来了结构问题。
callback 容器让顺序脱离语法,变成数据。
Promise 让这种数据结构标准化并引入状态。
async/await 则把这一切重新伪装成线性代码。
从这个角度看,现代异步模型并不是新的东西,而是对 callback 模型的一层层抽象与重构。