异步的本质:顺序与执行的拆分

通过个人理解,可以把异步问题拆成两部分:

  • 顺序:如何描述“接下来做什么”
  • 执行:谁来在合适的时机触发这些步骤

同步代码中,这两件事是绑定在一起的:

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 模型的一层层抽象与重构。