在 C++20 中,协程(coroutines)作为语言级特性被正式引入,提供了轻量级的异步编程模型。相比传统的基于线程或回调的方式,协程可以让代码保持同步风格,同时隐藏了异步执行的细节。本文将从头到尾演示如何使用 C++20 协程编写一个简单的异步 HTTP GET 请求,并说明关键实现细节与常见陷阱。
1. 先决条件
- 编译器:支持 C++20 的编译器(如 GCC 11+、Clang 13+ 或 MSVC 19.32+)。示例使用
g++ -std=c++20. - 依赖库:本例使用 libcurl 的多路复用接口
curl_multi,并配合asio事件循环(可使用boost::asio或standalone::asio)。 - 网络环境:能够访问
http://httpbin.org/get。
2. 基础协程包装
C++20 的协outine 需要一个 awaiter。下面我们先实现一个 CURLAwaiter,它把 curl_multi 的事件转换为可等待的事件。
#include <curl/curl.h>
#include <asio.hpp>
#include <future>
#include <iostream>
#include <memory>
#include <string>
struct CURLAwaiter {
CURLM* multi;
CURL* easy;
asio::io_context& io;
CURLAwaiter(CURLM* m, CURL* e, asio::io_context& ctx)
: multi(m), easy(e), io(ctx) {}
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
// 这里我们使用 asio 的 timer 来轮询 curl_multi 的完成事件
auto timer = std::make_shared<asio::steady_timer>(io, std::chrono::milliseconds(10));
timer->async_wait([h, this, timer](const asio::error_code& ec) mutable {
int running;
curl_multi_perform(multi, &running);
if (running == 0) {
// 所有请求完成
h.resume();
} else {
// 继续轮询
timer->expires_after(std::chrono::milliseconds(10));
timer->async_wait([h, this, timer](const asio::error_code& ec){});
}
});
}
std::string await_resume() {
char *data;
curl_easy_getinfo(easy, CURLINFO_PRIVATE, &data);
return std::string(data);
}
};
注意:上述代码是简化版,仅适用于单请求场景。真实项目需要管理多请求、错误处理、内存回收等。
3. 辅助函数:读取 curl 结果
size_t writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) {
std::string* str = static_cast<std::string*>(userdata);
str->append(ptr, size * nmemb);
return size * nmemb;
}
4. 主协程函数
#include <coroutine>
#include <exception>
class SimpleAwaitable {
public:
struct promise_type {
std::string result;
std::exception_ptr eptr;
SimpleAwaitable get_return_object() {
return SimpleAwaitable{ std::coroutine_handle <promise_type>::from_promise(*this) };
}
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void unhandled_exception() { eptr = std::current_exception(); }
void return_void() {}
};
std::coroutine_handle <promise_type> coro;
SimpleAwaitable(std::coroutine_handle <promise_type> h) : coro(h) {}
~SimpleAwaitable() { if (coro) coro.destroy(); }
};
SimpleAwaitable async_http_get(const std::string& url, asio::io_context& io) {
CURLM* multi = curl_multi_init();
CURL* easy = curl_easy_init();
std::string buffer;
curl_easy_setopt(easy, CURLOPT_URL, url.c_str());
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(easy, CURLOPT_WRITEDATA, &buffer);
curl_easy_setopt(easy, CURLOPT_PRIVATE, buffer.data()); // 传递给 await_resume
curl_multi_add_handle(multi, easy);
co_await CURLAwaiter(multi, easy, io);
// 这里 buffer 已经填满
std::cout << "HTTP Response length: " << buffer.size() << " bytes\n";
curl_multi_remove_handle(multi, easy);
curl_easy_cleanup(easy);
curl_multi_cleanup(multi);
}
5. 运行示例
int main() {
asio::io_context io;
auto task = async_http_get("http://httpbin.org/get", io);
// 启动 io_context 的事件循环
io.run();
return 0;
}
编译命令(假设 libcurl 和 asio 已正确安装):
g++ -std=c++20 -O2 -lcurl -lstdc++fs -pthread -I/path/to/asio/include main.cpp -o http_async
执行 ./http_async,你将看到类似:
HTTP Response length: 345 bytes
6. 常见坑与优化
-
协程悬挂:
await_suspend必须保证在协程被挂起后能够在某个时刻恢复。这里使用定时器轮询curl_multi_perform,在高并发场景下可能导致 CPU 占用过高。可以改为使用curl_multi_setopt(..., CURLMOPT_SOCKETFUNCTION, socketCallback)与curl_multi_setopt(..., CURLMOPT_TIMERFUNCTION, timerCallback),让 curl 在 socket 可读写时直接通知我们。 -
错误处理:示例中没有对
curl_easy_perform或curl_multi_perform的错误码做检查。生产环境请加入错误码判断并抛出异常或返回错误码。 -
资源管理:每个协程分配
CURLM、CURL对象,若并发量大需要统一管理或复用CURLM。建议使用线程池 + 协程的方式。 -
标准库协程:C++20 标准没有提供标准的协程包装器,所有协程包装器都需要自己实现。上述
SimpleAwaitable只是一个最小示例,实际中建议使用成熟库(如cppcoro、asio::awaitable等)来简化协程逻辑。 -
TLS/HTTPS:示例只演示了 HTTP。要支持 HTTPS,需要在
curl_easy_setopt中开启CURLOPT_SSL_VERIFYPEER、CURLOPT_SSL_VERIFYHOST并提供证书。
7. 小结
C++20 的协程让异步编程更接近同步写法,减少回调地狱和错误处理的复杂性。结合 libcurl 的多路复用接口与 asio 的事件循环,能够实现高效的异步 HTTP 客户端。虽然协程本身不提供网络 I/O,还是需要配合事件驱动框架或第三方库来实现完整功能。未来的 C++ 标准和生态可能会提供更直接的异步 I/O 支持,届时我们可以进一步简化实现。祝你在 C++20 的协程世界里玩得开心!