在现代 C++ 开发中,异步编程已经成为处理 I/O 密集型任务的主流方式。传统的回调、线程池或者基于事件循环的模型虽然各有优点,但在语义清晰、错误处理以及资源管理方面往往显得繁琐。C++20 通过引入协程(coroutine)这一语言特性,提供了更直观的方式来书写异步代码。本文将从协程的基本概念出发,演示如何在 C++20 中使用协程实现一个简易的异步网络请求框架,并讨论其性能优势和使用注意事项。
1. 协程基础
协程是可挂起的函数,可以在执行过程中暂停并保存其状态,待未来某个时刻再恢复。C++20 中协程的核心关键字有 co_await、co_yield、co_return,以及与之配合使用的 std::coroutine_handle。协程函数的返回类型不是普通的 int 或 void,而是一个具备 promise_type 的自定义类型。
1.1 协程函数声明
struct Task {
struct promise_type;
using handle_type = std::coroutine_handle <promise_type>;
struct promise_type {
Task get_return_object() { return {handle_type::from_promise(*this)}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::rethrow_exception(std::current_exception()); }
};
handle_type coro;
explicit Task(handle_type h) : coro(h) {}
~Task() { if (coro) coro.destroy(); }
};
Task async_operation(); // 这是一个协程函数
协程函数 async_operation 在被调用时并不会立即执行,而是返回一个 Task 对象,内部保存了协程句柄。只有当 coro.resume() 被显式调用时,协程才会开始执行。
1.2 co_await 的工作原理
co_await 用来等待一个可 Awaitable 的对象。可 Awaitable 的对象需要满足以下接口:
auto await_ready() -> bool;
auto await_suspend(std::coroutine_handle<>) -> void or bool;
auto await_resume() -> T;
await_ready():如果立即可用,返回true,协程不暂停。await_suspend():协程暂停,接收当前协程句柄,决定如何恢复。若返回true,协程将挂起,需由外部恢复;若返回false,协程立即恢复。await_resume():协程恢复后,返回结果。
2. 简易异步 I/O 适配器
C++20 标准库并未提供网络 I/O 的异步实现,常见做法是借助第三方库(如 Boost.Asio、libuv 等)或直接包装系统调用。下面以 POSIX epoll 为例,构造一个可 Awaitable 的异步读取器。
2.1 epoll 事件包装
struct EpollEvent {
int fd;
uint32_t events;
int epoll_fd;
EpollEvent(int f, uint32_t ev, int ef) : fd(f), events(ev), epoll_fd(ef) {}
bool await_ready() { return false; }
bool await_suspend(std::coroutine_handle<> h) {
epoll_event ev{ .events = events, .data.ptr = static_cast<void*>(h.address()) };
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
h.destroy(); // 发生错误,直接销毁协程
return false;
}
return true; // 挂起
}
int await_resume() {
epoll_event ev;
int nfds = epoll_wait(epoll_fd, &ev, 1, -1);
// epoll_wait 返回后,协程自动恢复
return nfds > 0 ? ev.events : -1;
}
};
2.2 异步读取器
Task async_read(int fd, std::vector <char>& buffer, int epoll_fd) {
EpollEvent ev{fd, EPOLLIN, epoll_fd};
co_await ev; // 等待可读事件
ssize_t n = read(fd, buffer.data(), buffer.size());
if (n < 0) throw std::system_error(errno, std::generic_category(), "read");
co_return;
}
3. 用协程实现 HTTP GET
接下来,演示一个简易的 HTTP GET 请求器。它使用 async_read 读取响应主体,并利用协程链式调用实现直观流程。
#include <iostream>
#include <vector>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
Task async_http_get(const std::string& host, const std::string& path, int epoll_fd) {
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) throw std::system_error(errno, std::generic_category(), "socket");
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(80);
inet_pton(AF_INET, host.c_str(), &addr.sin_addr);
if (connect(sock, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0)
throw std::system_error(errno, std::generic_category(), "connect");
std::string request = "GET " + path + " HTTP/1.1\r\nHost: " + host + "\r\nConnection: close\r\n\r\n";
write(sock, request.data(), request.size());
std::vector <char> buffer(4096);
while (true) {
co_await EpollEvent{sock, EPOLLIN, epoll_fd};
ssize_t n = read(sock, buffer.data(), buffer.size());
if (n <= 0) break;
std::cout.write(buffer.data(), n);
}
close(sock);
co_return;
}
4. 调度器与事件循环
协程本身只是一个“挂起/恢复”的机制,真正决定何时恢复的责任在于调度器。下面给出一个最小化的事件循环实现:
int main() {
int epoll_fd = epoll_create1(0);
if (epoll_fd < 0) return 1;
// 启动协程
auto task = async_http_get("93.184.216.34", "/", epoll_fd); // example.com
// 事件循环
while (true) {
epoll_event ev;
int n = epoll_wait(epoll_fd, &ev, 1, -1);
if (n <= 0) continue;
// 恢复挂起的协程
auto handle = static_cast<std::coroutine_handle<>>(ev.data.ptr);
handle.resume();
if (!handle.done()) continue;
handle.destroy(); // 任务完成,销毁句柄
break;
}
close(epoll_fd);
return 0;
}
5. 性能与优势
- 语义清晰:协程使异步流程写法接近同步代码,减少回调地狱。
- 低延迟:协程暂停时不产生线程上下文切换,只有在需要 I/O 完成时挂起/恢复。
- 资源友好:协程本身占用的栈空间极小,适合高并发场景。
6. 注意事项
- 异常传播:协程内部抛出的异常会被
promise_type::unhandled_exception()捕获并转发,需在调用点使用try-catch处理。 - 事件注册/注销:在
await_suspend与await_resume之间需要正确管理事件句柄,避免资源泄漏。 - 兼容性:C++20 协程仅在支持
-std=c++20的编译器(如 GCC 10+、Clang 12+、MSVC 19.28+)中可用。 - 第三方库:许多成熟的异步 I/O 框架(如
asio::awaitable)已经对协程进行了封装,直接使用可避免自行实现细节。
7. 结语
通过本示例,读者可以看到 C++20 协程如何与系统级 I/O 结合,打造既简洁又高效的异步网络程序。未来随着标准库对异步 I/O 的进一步完善,协程将成为 C++ 生态中不可或缺的编程范式。祝你在异步编程的路上越走越远!