因为std::coroutine仅提供语法糖,无调度器、i/o多路复用和socket生命周期管理,需搭配asio等异步库+自定义awaiter才能实现可用http服务器。

为什么不用 std::coroutine 直接写 HTTP 服务器
因为 C++20 的 std::coroutine 是纯语言设施,不带调度器、不带 I/O 多路复用、也不管 socket 生命周期。你写个 co_await,底层没地方挂起——没人监听 epoll 或 kqueue 事件,协程就永远卡在那儿。
真正能跑起来的协程 HTTP 服务,必须搭配异步 I/O 库(比如 libuv、boost.asio 或 uring 后端)+ 自定义 awaiter + 协程调度上下文。标准库只提供“语法糖”,不提供“运行时”。
-
std::suspend_always不能直接用于网络等待;它只是暂停,不是注册事件回调 - 没有
io_context或等效调度器时,co_await等同于同步阻塞(如果强行用 busy-loop 模拟,性能反不如线程池) - HTTP 解析、keep-alive 管理、buffer 复用这些逻辑,全得自己在协程栈上手动维护,稍有不慎就内存泄漏或状态错乱
boost::asio::use_awaitable 是目前最稳的落地路径
它把 asio 的异步原语(async_read、async_write、async_accept)包装成可 co_await 的对象,并自动绑定到 io_context 调度中。协程挂起时,实际是注册了 epoll/kqueue 回调;唤醒时,由 asio 的 event loop 把控制权交还给你。
关键不是“用了协程”,而是“协程和 asio 的 executor 绑定是否干净”。常见崩点在于:
立即学习“C++免费学习笔记(深入)”;
- 协程函数返回类型不是
boost::asio::awaitable<void></void>或带正确executor类型推导(比如漏掉.via(executor)) - 在协程里捕获
tcp::socket时用了值语义(auto sock = std::move(...)),但没确保它生命周期覆盖整个协程——asynccall 返回后 socket 可能已被销毁 - HTTP 请求解析用
boost::beast::http::parser时,buffer 必须是协程栈外持久的(比如用boost::beast::flat_buffer),否则协程挂起再恢复时 parser 指针失效
最小可行示例片段:
boost::asio::awaitable<void> handle_request(tcp::socket sock) {
boost::beast::flat_buffer buffer;
boost::beast::http::request<boost::beast::http::string_body> req;
auto& ex = co_await boost::asio::this_coro::executor;
try {
co_await boost::beast::http::async_read(sock, buffer, req, boost::asio::use_awaitable);
// ... 构造响应
co_await boost::beast::http::async_write(sock, res, boost::asio::use_awaitable);
} catch (...) { /* 注意:异常会终止协程,但 socket 需显式 close */ }
co_await sock.async_close(boost::asio::use_awaitable);
}
高并发下协程栈和内存分配容易被忽略
每个协程默认有几 KB 栈空间(boost::asio 默认 64KB),10 万连接 ≈ 6GB 内存纯开销。这不是理论值——实际压测中,malloc 频繁分配小块栈内存会成为瓶颈,尤其在容器化环境里触发 brk 锁争用。
- 必须用
boost::asio::experimental::make_coro_pool或自定义栈分配器,把协程栈从堆上切片复用 - 避免在协程内做任意长度的字符串拼接(如
std::string +=),每次扩容都可能触发堆分配;优先用boost::beast::string_view或预分配缓冲区 - HTTP header 解析结果别存为
std::map<:string std::string></:string>—— 插入/查找都涉及堆操作;用boost::beast::http::fields,它是紧凑内存布局的 hash 表
Linux 上 io_uring + 协程才是真正的轻量级组合
当连接数超 5 万、RTT 低于 1ms 时,epoll 的 syscall 开销开始明显。此时 io_uring 的无锁提交/完成队列 + 协程,才能榨出单核百万 QPS。但注意:liburing 本身不支持协程,需自己封装 awaiter。
核心难点不在 io_uring_submit,而在如何让一个协程等待特定 sqe 的 cqe 完成——你得维护 sqe index → 协程 promise* 的映射表,并在 io_uring_cqe_seen 后 resume 正确的协程。这个映射一旦错位,就是静默数据错乱。
- 不要复用同一个
io_uring实例跨线程调度协程;每个线程一个 ring,配合boost::asio::thread_pool分流 -
io_uring的IORING_OP_RECV不保证一次收完整个 HTTP request;仍需在协程里循环co_await直到解析完整报文 - 务必启用
IORING_SETUP_IOPOLL(仅限直连 NVMe)或IORING_SETUP_SQPOLL(需 root),否则性能不比 epoll 强
事情说清了就结束。










