C++20 协程是 C++ 标准库和语言层面的一大创新,为异步编程提供了一种更为直观、内聚的实现方式。它既兼容传统的回调、Future/Promise 方案,也为更复杂的控制流(如生成器、协程管道)提供了天然支持。本文将系统地介绍协程的概念、实现细节,并通过一个完整的异步 I/O 示例展示如何在实际项目中使用。
1. 协程的基本概念
在 C++20 之前,异步操作往往通过回调函数或多线程的方式实现。协程的核心思想是“暂停”和“恢复”函数的执行,形成一个可以在不同时间点切换的执行单元。协程的运行机制可以用以下几个关键词来概括:
co_await:等待一个异步操作完成,并将执行挂起。co_yield:在生成器中返回一个值,但保留执行状态以供后续恢复。co_return:结束协程,返回最终结果。
协程本身是一个特殊的函数,返回类型必须是 std::future、std::generator 或用户自定义的“协程类型”。编译器在编译时会把协程拆分成若干个小状态机,隐藏在内部实现细节。
2. 协程的实现细节
2.1 协程句柄(std::coroutine_handle)
协程句柄是协程内部状态机的入口,拥有:
template<class Promise>
class coroutine_handle {
public:
void resume(); // 继续执行
bool done() const; // 检查是否完成
Promise& promise(); // 访问协程 Promise 对象
};
编译器会为每个协程生成一个对应的 promise_type,其中存放协程的局部状态和返回值。
2.2 Promise 对象
promise_type 用于在协程暂停时保存状态,或在协程结束时提供返回值。典型的 promise_type 需要实现:
struct promise_type {
std::coroutine_handle <promise_type> get_return_object();
std::suspend_never initial_suspend(); // 初始挂起策略
std::suspend_always final_suspend(); // 结束挂起策略
void return_value(T value); // 传递返回值
void unhandled_exception(); // 异常处理
};
通过自定义 promise_type,我们可以控制协程何时挂起、返回什么类型的对象以及异常如何传播。
2.3 Suspend / Yield 策略
std::suspend_always:每次调用都会挂起,适合需要显式挂起的地方。std::suspend_never:永不挂起,适合不需要暂停的地方。
在协程中,co_await 会根据被等待对象的 await_ready()、await_suspend()、await_resume() 三个成员函数来决定是否挂起。
3. 一个完整的异步 I/O 示例
下面演示如何使用协程实现一个简易的异步文件读取器,利用 C++20 的标准库和 Boost.Asio。
3.1 准备工作
sudo apt-get install libboost-all-dev
3.2 代码实现
// async_file_reader.cpp
#include <iostream>
#include <fstream>
#include <string>
#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/thread_pool.hpp>
using namespace boost::asio;
using namespace std::chrono_literals;
namespace asio = boost::asio;
using boost::asio::awaitable;
using boost::asio::use_awaitable;
// 读取文件内容的协程
awaitable<std::string> read_file(const std::string& path) {
// 使用一个异步线程池
thread_pool pool(1);
// 打开文件
co_await pool.async_spawn([path](awaitable <void> self) -> awaitable<void> {
std::ifstream ifs(path, std::ios::binary);
if (!ifs) {
throw std::runtime_error("无法打开文件");
}
// 读取文件内容
std::string content((std::istreambuf_iterator <char>(ifs)), std::istreambuf_iterator<char>());
// 通过 co_return 返回
co_return content;
}, use_awaitable);
}
// 主协程入口
awaitable <void> main_co() {
try {
std::string data = co_await read_file("sample.txt");
std::cout << "文件内容长度: " << data.size() << " 字节\n";
std::cout << "前 100 字符: " << data.substr(0, 100) << "\n";
} catch (const std::exception& e) {
std::cerr << "异常: " << e.what() << "\n";
}
}
int main() {
// 运行协程
asio::io_context io;
asio::co_spawn(io, main_co(), asio::detached);
io.run();
return 0;
}
3.3 运行与效果
g++ -std=c++20 -pthread async_file_reader.cpp -lboost_system -lboost_thread -o async_reader
./async_reader
程序会异步读取 sample.txt 并打印前 100 个字符,展示了协程如何在后台线程中执行 I/O 而不阻塞主线程。
4. 协程与传统异步方案的对比
| 特性 | 传统回调 | std::future / Promise | 协程 |
|---|---|---|---|
| 可读性 | 低 | 中 | 高 |
| 错误传播 | 手动 | 自动 | 自动 |
| 代码复用 | 低 | 中 | 高 |
| 性能 | 依赖线程/事件循环 | 轻量 | 极轻量(仅状态机) |
协程最大的优势在于让异步代码看起来像同步代码,避免了“回调地狱”或“Future 链式调用”导致的代码混乱。
5. 常见陷阱与注意事项
- 生命周期管理:协程对象和其内部
promise_type的生命周期紧密绑定,必须确保协程句柄不被提前销毁。 - 异常安全:在协程中抛出的异常会被封装进
future,使用co_await时需捕获。 - 资源释放:如果协程在异步操作中占用了资源(如文件句柄),应在
final_suspend或co_return处显式释放,防止泄漏。 - 线程安全:协程本身不保证线程安全,若在多线程中共享协程对象,需要自行同步。
6. 结语
C++20 协程为语言带来了更强的异步表达能力,让复杂的异步控制流变得简单易读。无论是网络编程、文件 I/O,还是游戏循环,协程都能提供更为直观的实现方式。随着库生态(如 Boost.Asio、cppcoro 等)的完善,协程正逐渐成为现代 C++ 开发的核心技术之一。希望本文能帮助你快速上手并将协程融入实际项目。