当年我们写异步。
写得很像拆炸弹。
一根线接一根线。
剪错一根。
要么崩。
要么死等。
而最折磨人的。
不是崩。
是你不知道它为什么在等。
当年:回调是“真实世界的接口”
IO 完成这件事。
本来就发生在未来。
所以系统给你的接口。
很自然就是。
“好了我叫你”。
using Callback = std::function<void(int status, std::string body)>;
void http_get(std::string url, Callback cb);
你没法让它同步。
因为它真的在等网络。
线上啪一下:重试 + 超时 + 兜底,回调树长出来了
你写一个小项目。
拉一个配置。
失败重试一次。
超时就走降级。
你很快会写成这样。
void fetch_config() {
http_get("/config", [&](int st, std::string body) {
if (st != 200) {
http_get("/config", [&](int st2, std::string body2) {
if (st2 != 200) {
// fallback
return;
}
// parse(body2)
});
return;
}
// parse(body)
});
}
它能跑。
但它不再像“步骤”。
它像一堆跳转。
你读的时候。
得在脑子里把它拉直。
协程的办法:把回调变成 awaitable
你不一定要立刻有完整的异步 runtime。
你可以先学会一招。
把回调包装成一个对象。
让它能被 co_await。
我们先写一个最小的 awaitable。
#include <coroutine>
#include <string>
struct HttpResult {
int status;
std::string body;
};
struct HttpAwaitable {
std::string url;
HttpResult result{};
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h);
HttpResult await_resume() noexcept { return result; }
};
这三个函数。
你可以先按名字理解。
ready:要不要等。
suspend:我要等,那就把控制权交出去。
resume:等完了,把结果拿回来。
用它:异步逻辑回到“直线”
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() noexcept {}
void unhandled_exception() { std::terminate(); }
};
};
Task fetch_config() {
auto r = co_await HttpAwaitable{"/config"};
if (r.status != 200) {
auto r2 = co_await HttpAwaitable{"/config"};
if (r2.status != 200) co_return;
// parse(r2.body)
co_return;
}
// parse(r.body)
}
你看。
逻辑还是重试。
但它回到了“从上到下”。
你不再需要在 lambda 里跳来跳去。
关键结论
协程在异步里最核心的价值。
不是让 IO 变快。
是让你的控制流。
重新可读。
小结
先别急着写框架。
先学会把回调包装成 awaitable。
你就已经把最痛的那部分。
解决了一半。