C++20协程仅提供挂起/恢复机制,不内置异步调度;需手动实现awaiter并桥接epoll等系统调用,配合非阻塞socket与事件循环才能真正异步IO。

协程在 C++20 中不是“开箱即用”的异步调度器
直接写 co_await 不会自动发起 HTTP 请求或处理 IO;C++20 协程只提供挂起/恢复的底层机制,没有内置事件循环、线程池或 socket 封装。你得自己桥接系统调用(如 epoll、select 或 Windows IOCP),否则协程只是同步执行的语法糖。
常见错误是以为 co_await 本身能“让出线程”并等网络就绪——它不会。若底层 awaiter 的 await_ready() 返回 true,协程根本不会挂起;若返回 false 但没注册等待逻辑(比如没把 fd 加入 epoll 监听),协程就会永久挂起。
- 必须实现自己的
awaiter类型,并在await_suspend()中注册 IO 就绪通知 - 必须维护一个运行时(哪怕只有一个线程的轮询 loop),驱动挂起的协程恢复
- 所有 socket 操作需设为非阻塞模式(
fcntl(fd, F_SETFL, O_NONBLOCK)),否则read/write会阻塞整个协程线程
如何手动实现一个可 await 的 socket read
以 Linux 下读取 TCP socket 为例:核心是让协程在 read() 返回 EAGAIN 时挂起,并在 epoll 报告该 fd 可读时恢复。
关键点在于 await_suspend() 必须把当前协程 handle 存入 epoll 对应 fd 的上下文(比如用 std::map<int std::coroutine_handle>></int> 缓存),否则事件来了找不到谁该唤醒。
立即学习“C++免费学习笔记(深入)”;
struct socket_reader {
int fd;
std::span<std::byte> buf;
bool await_ready() const noexcept {
// 尝试非阻塞读,成功则不挂起
ssize_t n = ::read(fd, buf.data(), buf.size());
if (n > 0) {
bytes_read = n;
return true;
}
if (n == 0 || errno != EAGAIN && errno != EWOULDBLOCK)
throw std::system_error{errno, std::generic_category()};
return false;
}
void await_suspend(std::coroutine_handle<> h) noexcept {
// 注册到 epoll:fd → h 映射需线程安全(单线程可裸用 map)
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev); // ev.events = EPOLLIN
fd_to_handler[fd] = h; // 全局或 loop 实例持有
}
ssize_t await_resume() const noexcept { return bytes_read; }
};
-
await_ready()必须做一次真实read(),不能只查epoll_wait()结果——因为可能有数据已在内核缓冲区 -
epoll_ctl(..., EPOLL_CTL_ADD)在每次await_suspend()都调用是错的:重复添加会失败;应确保只注册一次,或改用EPOLL_CTL_MOD - 恢复后需立刻从 epoll 移除监听(
EPOLL_CTL_DEL)或重置事件,否则下次await可能立即触发
为什么不要手写完整 HTTP 客户端协程封装
HTTP 协议层本身不难,但“轻量级”和“可靠”在此冲突:你需要处理分块传输、重定向跳转、连接复用(keep-alive)、TLS 握手挂起、超时取消、DNS 解析异步化……每一条都会把你拉回轮子深坑。
例如 DNS 解析:Linux 的 getaddrinfo_a() 是 GNU 扩展,不可移植;Windows 的 GetAddrInfoExA() 又是异步回调风格,和协程 awaiter 风格不兼容——你得再包一层 promise/future 转发,失去“轻量”本意。
- 纯协程 HTTP 客户端在无库前提下,仅适用于固定域名、HTTP/1.0、无 TLS、无重试的玩具场景
- 若真要生产可用,
std::jthread+std::promise包一层curl的异步接口,比手写协程调度器更轻、更稳 - C++23 的
std::generator和std::ranges::async提案仍未落地,别指望标准库补位
真正轻量的替代方案:只协程化业务逻辑,IO 交给系统 API
与其让协程直接操作 socket,不如把网络部分抽成同步风格函数(内部用非阻塞 + epoll loop 驱动),再用协程组织调用顺序。这样协程代码干净,IO 细节隔离。
示例:一个请求函数返回 std::future<std::string>,你在协程里 co_await 它的 std::future 特化 awaiter(C++20 标准支持):
// 外部已有的轻量 IO loop(比如单线程 epoll + 回调队列)
auto http_get(std::string_view url) -> std::future<std::string>;
// 协程中自然使用
task<void> fetch_and_log() {
auto body = co_await http_get("http://example.com");
std::cout << body.size() << " bytes\n";
}
- 这个方案下,你只需实现一个符合
std::future规范的 awaiter(几行模板),不用碰 epoll 注册/注销细节 - IO loop 可复用已有成熟小轮子(如
libuv的简化版,或uring的 sync wrapper),不增加新依赖 - 协程异常传播、栈展开、取消点都由标准
std::future保证,比手写 awaiter 更少出错
真正的复杂点不在协程语法,而在如何让一次网络 IO 的生命周期与协程生命周期对齐:fd 生命周期、缓冲区所有权、错误码映射——这些地方一漏,就是静默崩溃或内存泄漏。


















