在现代 C++ 开发中,异步编程已成为提高程序并发性和响应性的关键技术。传统的异步实现往往依赖回调函数、Future/Promise、线程池等机制,代码往往冗长且难以维护。C++20 引入了协程(coroutine)这一强大的语言特性,为异步编程带来了全新的表达方式。本文将从协程的基本概念、实现机制、与传统异步编程的差异,以及实际应用场景等方面进行深入探讨。
一、协程概念与语法基础
协程是一种轻量级的子程序,可以在执行过程中挂起(suspend)并在后续恢复(resume),从而实现非阻塞的异步逻辑。C++20 对协程的支持主要体现在以下关键字和类型上:
| 关键字/类型 | 说明 |
|---|---|
co_await |
用于等待一个 awaitable 对象(如 std::future、自定义 Awaitable) |
co_yield |
在协程内部产生一个值,通常与 generator 配合使用 |
co_return |
结束协程并返回值 |
std::suspend_always / std::suspend_never |
生成器的挂起策略 |
协程本身并不是线程;它们在同一线程上调度。挂起点是协程执行的“点”,从而实现非阻塞的等待。
二、协程的实现细节
1. Awaitable 对象
协程通过 co_await 关键字等待一个 Awaitable 对象。一个对象只需要满足以下接口即可:
struct Awaitable {
bool await_ready(); // 是否可以立即完成
void await_suspend(std::coroutine_handle<>) // 挂起时的操作
auto await_resume(); // 挂起恢复后返回的结果
};
如果 await_ready() 返回 true,协程会立即继续执行,否则 await_suspend 被调用,协程挂起。
2. 协程句柄(coroutine_handle)
协程句柄是 C++20 统一的协程入口点,负责协程的创建、挂起、恢复与销毁。常见的句柄类型:
std::coroutine_handle<>;
std::coroutine_handle <promise_type>;
在 co_await、co_yield、co_return 等处,编译器会自动生成 promise_type。
3. Promise 类型
协程的返回值、异常等信息通过 promise_type 传递。用户需要实现:
struct promise_type {
auto get_return_object(); // 返回协程对象
std::suspend_always initial_suspend(); // 初始挂起策略
std::suspend_always final_suspend(); // 终止挂起策略
void return_value(T value); // 返回值
void unhandled_exception(); // 未处理异常
};
三、传统异步编程与协程的比较
| 维度 | 传统方式(回调 / Future) | 协程 |
|---|---|---|
| 可读性 | 回调嵌套导致“回调地狱”,难以追踪流程 | 代码几乎与同步代码一致,逻辑清晰 |
| 错误处理 | 异常需通过错误码或回调传递 | 可使用 try/catch 直接捕获异常 |
| 资源管理 | 需要手动管理线程池、同步原语 | 协程内部由语言和标准库管理,减少资源泄漏 |
| 性能 | 线程切换成本高,线程池管理开销 | 协程调度在用户空间,切换开销极低 |
| 兼容性 | 与旧代码兼容性好 | 需要编译器支持 C++20 或后续版本 |
四、实战案例:协程实现一个简易 HTTP 客户端
下面演示一个使用协程读取 std::future 的示例,模拟异步 HTTP 请求。
#include <iostream>
#include <future>
#include <chrono>
#include <coroutine>
// 简易的 Awaitable:包装 std::future
template<typename T>
struct FutureAwaiter {
std::future <T> fut;
bool await_ready() { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }
void await_suspend(std::coroutine_handle<> h) {
std::thread([h, this](){ fut.wait(); h.resume(); }).detach();
}
T await_resume() { return fut.get(); }
};
template<typename T>
FutureAwaiter <T> awaitable(std::future<T> f) { return {std::move(f)}; }
// 协程函数
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
Task http_get(const std::string& url) {
std::cout << "开始请求: " << url << std::endl;
// 模拟异步 I/O,使用 std::async
std::future<std::string> fut = std::async(std::launch::async, [url]{
std::this_thread::sleep_for(std::chrono::seconds(2));
return std::string("响应来自 ") + url;
});
std::string response = co_await awaitable(std::move(fut));
std::cout << "收到响应: " << response << std::endl;
co_return;
}
int main() {
http_get("http://example.com");
std::this_thread::sleep_for(std::chrono::seconds(3));
return 0;
}
该示例中,http_get 通过 co_await 等待 std::future,代码结构与同步编程极为相似。协程内部隐藏了线程池与线程切换的细节,提升了可读性与可维护性。
五、协程的局限与注意事项
- 编译器支持:协程是 C++20 标准的一部分,需使用支持协程的编译器(如 GCC 10+、Clang 10+、MSVC 19.28+)。
- 资源占用:虽然协程切换成本低,但每个协程仍需一定堆栈空间,若协程数量过多需注意内存消耗。
- 错误传播:协程内部的异常需要
promise_type处理,否则会导致程序崩溃。最好使用try/catch捕获异步错误。 - 标准库支持:截至 C++20,标准库中对协程的支持仍有限,许多实用工具(如
generator、task)仍需第三方库(如 cppcoro、Boost.Coroutine2)实现。
六、结语
C++20 协程为异步编程提供了一种更直观、更易维护的表达方式。它将异步流程“平铺”成同步的语义,使代码更易阅读、调试和错误追踪。虽然在实际项目中仍需结合现有库和平台限制,但掌握协程的核心概念与编写技巧,将极大提升 C++ 开发者在高性能、高并发场景下的工作效率。希望本文能为你在 C++ 生态中迈向协程时代提供有益参考。