在现代C++中,协程(coroutine)为我们提供了一种简洁而强大的方式来编写异步、事件驱动或并发代码。自C++20起,协程被正式纳入标准库,配合std::generator、std::task、std::suspend_always等辅助类,让异步编程更像同步代码。本文将深入剖析协程的工作原理、关键概念以及实际使用技巧,并通过示例代码展示如何在C++20项目中实现异步任务。
1. 协程的基本概念
- 协程函数:一种特殊的函数,它可以在执行过程中被挂起和恢复。协程函数的声明需要返回类型为
std::experimental::coroutine_handle相关的类型,例如std::future,std::generator等。 - 挂起点:协程内部使用
co_await,co_yield或co_return语句触发挂起。挂起时,协程的局部状态会被保存,直到下次恢复。 - 协程句柄:
coroutine_handle是对协程实例的可操作句柄,负责调度协程的执行和销毁。
2. 关键实现细节
2.1 协程的状态机
编译器会把协程函数编译成一个状态机。每个挂起点对应一个状态值,协程在恢复时根据当前状态决定执行哪一段代码。
2.2 协程框架中的promise_type
每个协程都有一个与之关联的promise_type。它是协程的“宿主”,负责提供协程的返回值、异常处理以及挂起点的行为。promise_type需要实现以下成员:
get_return_object()initial_suspend()final_suspend()return_value(T)unhandled_exception()
2.3 co_await、co_yield和co_return
co_await expr:等待expr的完成。expr必须返回一个可awaitable的对象。co_yield expr:生成一个值并挂起协程,等待下一个co_yield或外部调用。co_return expr:结束协程,返回最终值。
3. 常用协程包装
3.1 std::generator
std::generator <int> count_up_to(int n) {
for (int i = 1; i <= n; ++i)
co_yield i;
}
3.2 std::task(自定义)
标准库中并未直接提供task,但可以自定义一个类似于std::future的协程返回类型。
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::rethrow_exception(std::current_exception()); }
};
};
4. 示例:异步下载文件
下面演示如何使用协程实现一个简单的异步下载器。我们将使用cppcoro::async_http_client(第三方库)来演示协程与网络 I/O 的结合。
#include <cppcoro/async_task.hpp>
#include <cppcoro/sync_wait.hpp>
#include <cppcoro/async_pipe.hpp>
#include <cppcoro/http_client.hpp>
#include <filesystem>
#include <fstream>
#include <iostream>
namespace fs = std::filesystem;
cppcoro::async_task <void> download_file(const std::string& url, const fs::path& out_path) {
cppcoro::http_client client(url);
co_await client.send_request(cppcoro::http::verb::get);
std::ofstream ofs(out_path, std::ios::binary);
if (!ofs.is_open()) {
std::cerr << "Failed to open output file.\n";
co_return;
}
auto stream = client.body_stream();
while (auto chunk = co_await stream.next()) {
ofs.write(chunk.data(), chunk.size());
}
std::cout << "Download completed: " << out_path << "\n";
}
int main() {
cppcoro::sync_wait(download_file("https://example.com/file.bin", "file.bin"));
}
说明
- `cppcoro::async_task ` 是一个协程返回类型,类似于 `std::future`。
co_await client.send_request(...)让协程挂起,等待 HTTP 请求完成。stream.next()挂起直到下一个数据块可用,实现流式读取。
5. 常见陷阱与调试技巧
- 忘记
initial_suspend或final_suspend:如果返回std::suspend_always,协程会在创建时立即挂起,需要手动调用handle.resume()。 - 异常泄露:
promise_type::unhandled_exception()必须处理异常,否则会导致未定义行为。 - 资源泄漏:协程结束前请确保所有资源(如文件句柄、网络连接)已释放。
- 调试工具:IDE 里可以通过设置断点在
co_await行查看挂起点;或者使用std::experimental::coroutine_traits打印状态机信息。
6. 性能考量
- 协程的开销:与传统回调相比,协程的上下文切换更轻量,且避免了大量堆分配。
- 内存占用:协程的局部变量会在协程框架中存储;如果变量很大,考虑使用
std::shared_ptr或std::unique_ptr。 - 与线程池结合:可以将协程与线程池(如
cppcoro::io_context)结合,让 I/O 任务在后台线程上运行。
7. 结语
C++20 的协程为我们提供了一种与同步代码风格相近、易于维护的异步编程方式。通过理解其背后的状态机、promise_type 以及挂起点,开发者可以在不牺牲性能的前提下实现复杂的异步逻辑。未来的标准库(C++23 之后)将进一步完善协程相关工具,如 std::generator、std::task 等,让协程生态更加完整。掌握这些技术,您将能在网络编程、游戏开发、金融交易等领域写出更高效、可读性更好的代码。