在 C++20 标准正式发布后,协程(coroutines)成为了一个强大的语言特性,它让异步编程变得像同步编程一样直观。本文将以一个实际的网络请求示例为核心,演示如何在 C++20 环境下使用 co_await、co_return、std::generator 等关键字来实现高并发网络爬虫,并在此基础上探讨性能优化与错误处理的最佳实践。
一、协程的基本概念
协程是可以挂起(suspend)并在之后恢复执行的函数。C++20 中的协程是通过 co_await、co_yield、co_return 三个关键字实现的。协程函数的返回类型必须是 `std::experimental::generator
`、`std::future` 或自定义的 Promise 类。协程本身不负责调度,它们被包装成一个可挂起的状态机,外部的执行器负责决定何时恢复。
### 二、构建协程网络请求
假设我们使用 `cppcoro`(一个开源的协程库)来实现异步网络 I/O。以下代码展示了如何封装一个异步 HTTP GET 请求:
“`cpp
#include
#include
#include
#include
#include
#include
using namespace cppcoro;
using namespace std::chrono_literals;
// 异步请求函数
task fetch_url(std::string url, cancellation_token ct = {})
{
// 创建 HttpClient 并发起请求
auto client = http_client(url, ct);
auto resp = co_await client.get(); // 这里会挂起直到响应完成
if (!resp.success())
{
throw std::runtime_error(“HTTP error: ” + std::to_string(resp.status()));
}
// 读取响应主体
std::string body = co_await resp.read_string(); // 再次挂起
co_return std::move(body);
}
“`
`task
` 是一个轻量级的协程返回类型,类似于 `std::future`,但不需要线程池即可完成挂起/恢复。`cancellation_token` 为取消功能提供支持。
### 三、并发执行与调度
如果需要并发抓取多个页面,可以利用 `cppcoro::when_all` 来同时等待多个协程完成:
“`cpp
#include
#include
task<std::vector> fetch_all(const std::vector& urls)
{
std::vector<task> tasks;
for (const auto& url : urls)
tasks.emplace_back(fetch_url(url));
auto results = co_await when_all(std::move(tasks)); // 等待所有任务
std::vector bodies;
for (auto& result : results)
bodies.push_back(std::move(result.get()));
co_return std::move(bodies);
}
“`
主程序中使用 `sync_wait` 进行同步等待:
“`cpp
int main()
{
std::vector urls = {
“http://example.com”,
“http://example.org”,
“http://example.net”
};
try
{
auto bodies = sync_wait(fetch_all(urls));
for (const auto& body : bodies)
std::cout << body.substr(0, 100) << "\n—\n";
}
catch (const std::exception& e)
{
std::cerr << "Error: " << e.what() << "\n";
}
return 0;
}
“`
### 四、错误处理与超时控制
协程天然支持 `try/catch` 语法。可以在 `fetch_url` 内部捕获网络层面的异常,然后抛出自定义错误:
“`cpp
task fetch_url(std::string url, cancellation_token ct = {})
{
try
{
auto client = http_client(url, ct);
auto resp = co_await client.get();
if (!resp.success())
throw std::runtime_error(“HTTP error: ” + std::to_string(resp.status()));
std::string body = co_await resp.read_string();
co_return std::move(body);
}
catch (const std::exception& e)
{
// 包装错误信息返回给调用者
throw std::runtime_error(“Failed to fetch ” + url + “: ” + e.what());
}
}
“`
对超时的控制可以结合 `cppcoro::cancellation_token_source`:
“`cpp
task fetch_url_with_timeout(std::string url, std::chrono::milliseconds timeout)
{
cppcoro::cancellation_token_source cts;
auto ct = cts.token();
// 计时器
co_await std::suspend_always{}; // 这里会让协程挂起
// 这里省略计时器实现,假设在 timeout 后调用 cts.cancel();
return co_await fetch_url(url, ct);
}
“`
### 五、性能与资源管理
1. **无线程阻塞**:协程本身不占用线程,只有真正需要 I/O 的时刻才挂起,减少线程切换开销。
2. **按需调度**:`cppcoro::when_all` 可以在多核 CPU 上并行执行,但每个协程仍在单线程中执行 I/O,避免了锁竞争。
3. **内存占用**:协程的状态机在栈上实现,栈帧大小可通过编译器选项调优;若担心堆碎片,可使用自定义 `std::pmr::memory_resource`。
4. **取消与资源释放**:`cancellation_token` 能够及时中断挂起状态,并在 `co_await` 时触发 RAII 清理。
### 六、结语
C++20 的协程为高并发网络编程提供了新的语法糖,使得代码既简洁又安全。通过使用 `cppcoro` 或标准库的 `std::experimental::generator`,我们可以在不牺牲性能的前提下,构建可维护且可扩展的异步系统。未来的 C++20 协程生态将继续成熟,结合 `std::thread`、`std::execution`、`std::ranges` 等特性,期待能在更广泛的领域展现其强大魅力。</task</std::vector