在C++20中,协程(coroutine)作为一种轻量级的异步编程工具,为开发者提供了接近同步代码的编写方式,同时保持了高并发和低延迟的性能优势。本文将从协程的基本概念入手,结合Windows异步IO(Overlapped IO)和Linux的epoll,阐述如何在实际项目中使用C++20协程实现高效的异步网络IO。
1. 协程基础回顾
C++20协程的核心是co_await、co_yield和co_return三个关键字。它们配合协程句柄(std::coroutine_handle)以及约定的返回类型(如std::future、std::generator等)形成完整的协程系统。
- 挂起点:
co_await可以挂起协程,等待外部事件完成后再恢复。 - 恢复点:当事件完成时,协程句柄会被唤醒,继续执行后续代码。
- 异常处理:协程内的异常会通过协程返回值传递给调用者,或者通过自定义异常处理器捕获。
2. 设计协程包装器
为了让协程直接与平台异步IO交互,需要设计一个统一的“异步操作”包装器。下面以Windows Overlapped IO为例:
#include <windows.h>
#include <coroutine>
#include <future>
#include <iostream>
struct IOAwaitable {
HANDLE hFile;
OVERLAPPED ov;
DWORD bytesRead;
IOAwaitable(HANDLE h, std::size_t bufferSize)
: hFile(h), ov{}, bytesRead(0)
{
ZeroMemory(&ov, sizeof(ov));
}
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) noexcept {
// 启动异步读取
if (!ReadFile(hFile, nullptr, 0, nullptr, &ov)) {
if (GetLastError() != ERROR_IO_PENDING) {
std::terminate(); // 处理错误
}
}
// 保存句柄,以便在完成时唤醒
ov.hEvent = reinterpret_cast <HANDLE>(h.address());
}
DWORD await_resume() noexcept {
// 等待事件完成
DWORD res = 0;
GetOverlappedResult(hFile, &ov, &res, FALSE);
return res;
}
};
调用方式:
async<std::future<int>> asyncRead(HANDLE hFile, void* buffer, std::size_t size) {
IOAwaitable a(hFile, size);
co_return co_await a;
}
3. 与epoll的协同
在Linux环境下,epoll为多路复用提供了高效机制。结合C++20协程,可以将epoll_wait包装为awaitable:
struct EpollAwaitable {
int epfd;
int fd;
std::coroutine_handle<> handle;
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) noexcept {
handle = h;
struct epoll_event ev{};
ev.events = EPOLLIN;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
// 使用自定义事件循环来唤醒协程
// 这里省略实现细节
}
int await_resume() noexcept {
// 返回可读字节数或错误码
// 这里假设已完成
return 0;
}
};
在事件循环中,一旦epoll_wait检测到fd可读,即可通过handle.resume()恢复相应协程。
4. 示例:异步HTTP客户端
以下是一个利用上述协程包装器实现的简易异步HTTP客户端示例(Windows):
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#include <coroutine>
#include <future>
#include <iostream>
#include <string>
struct AsyncSocket {
SOCKET sock;
OVERLAPPED ov;
char buffer[4096];
AsyncSocket(SOCKET s) : sock(s), ov{} { ZeroMemory(&ov, sizeof(ov)); }
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) noexcept {
if (!WSARecv(sock, nullptr, 0, nullptr, &ov)) {
if (WSAGetLastError() != WSA_IO_PENDING) {
std::terminate();
}
}
ov.hEvent = reinterpret_cast <HANDLE>(h.address());
}
int await_resume() noexcept {
DWORD bytes = 0;
WSAGetOverlappedResult(sock, &ov, &bytes, FALSE, nullptr);
return static_cast <int>(bytes);
}
};
async<std::future<std::string>> httpGet(const std::string& host, const std::string& path) {
// 初始化Winsock
WSADATA wsaData; WSAStartup(MAKEWORD(2,2), &wsaData);
// 创建并连接套接字
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in server{};
server.sin_family = AF_INET;
inet_pton(AF_INET, host.c_str(), &server.sin_addr);
server.sin_port = htons(80);
connect(sock, (sockaddr*)&server, sizeof(server));
// 发送请求
std::string request = "GET " + path + " HTTP/1.1\r\nHost: " + host + "\r\nConnection: close\r\n\r\n";
send(sock, request.c_str(), static_cast <int>(request.size()), 0);
AsyncSocket asock(sock);
int bytesRead = co_await asock;
std::string response(asock.buffer, bytesRead);
closesocket(sock);
WSACleanup();
co_return response;
}
此示例展示了如何使用协程简化异步IO流程,减少回调地狱并保持代码的可读性。
5. 性能与资源管理
- 句柄泄漏:确保所有异步操作完成后及时关闭句柄。
- 错误处理:在
await_resume中检查错误码并抛出异常或返回错误状态。 - 并发控制:使用线程池或任务队列限制并发数量,防止资源耗尽。
6. 结语
C++20协程为异步IO编程带来了前所未有的简洁与强大。通过合适的包装器,将平台异步API与协程无缝结合,既能保留底层性能优势,又能提升代码可维护性。未来随着标准进一步完善,协程将成为C++高性能网络应用的首选工具。