C++20 中协程的实战:如何用 async/await 编写高并发程序

在 C++20 标准正式发布后,协程(coroutines)成为了一个强大的语言特性,它让异步编程变得像同步编程一样直观。本文将以一个实际的网络请求示例为核心,演示如何在 C++20 环境下使用 co_awaitco_returnstd::generator 等关键字来实现高并发网络爬虫,并在此基础上探讨性能优化与错误处理的最佳实践。

一、协程的基本概念

协程是可以挂起(suspend)并在之后恢复执行的函数。C++20 中的协程是通过 co_awaitco_yieldco_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

发表评论