C++20 在标准库中正式引入了协程(coroutine)这一强大的语法和运行时特性。它的核心目标是让我们可以在保持同步代码可读性的前提下,轻松实现异步、惰性计算或状态机等复杂控制流。下面从概念、实现细节和实战示例三部分,系统地讲解协程的使用方式。
1. 协程的基本概念
-
协程与线程的区别
- 线程是操作系统级别的并发单元,切换成本高,且每个线程需要单独的栈空间。
- 协程是语言级别的轻量级并发,多个协程共用同一线程的栈空间,切换只需保存和恢复程序计数器(即“挂起点”)即可。
-
协程的生命周期
- 创建:调用协程函数时,编译器会把函数拆成一个状态机。
- 挂起(
co_await、co_yield、co_return):协程在执行到这些关键字时会暂存状态并返回控制给调用者。 - 恢复:调用者通过
resume重新激活协程,恢复到上一次挂起的位置继续执行。
-
协程返回类型
- 协程函数的返回类型必须满足
std::experimental::coroutine_traits或自定义的协程返回类型。 - 标准库提供了
std::future,std::generator,std::task(可用在 C++23)等。
- 协程函数的返回类型必须满足
2. C++20 协程的实现细节
-
关键字
co_await:等待一个 awaitable 对象完成。co_yield:向外部“产生”一个值,暂停执行。co_return:结束协程并返回结果。
-
awaitable 对象
一个对象若要被co_await,必须实现以下成员:bool await_ready(); // 是否立即完成 void await_suspend(std::coroutine_handle<> h); // 若未完成,挂起协程 auto await_resume(); // 结果返回常见实现:
std::future,std::promise, 自定义异步 I/O 对象等。 -
协程句柄(
std::coroutine_handle)
句柄是协程的引用类型,用来控制协程的挂起、恢复与销毁。 -
**生成器(`std::generator
`)** 通过 `co_yield` 产生序列值,调用者使用 `begin()/end()` 迭代。
3. 简易示例:异步读取文件
下面用标准库(C++20)编写一个异步读取文件的协程示例,演示如何结合 std::filesystem 和 std::async。
#include <iostream>
#include <coroutine>
#include <vector>
#include <fstream>
#include <sstream>
#include <future>
#include <chrono>
// 1. awaitable: 异步文件读取
struct async_read
{
std::string path;
std::vector <char> buffer;
std::coroutine_handle<> h; // 协程句柄,用于挂起与恢复
async_read(std::string p) : path(std::move(p)) {}
bool await_ready() noexcept { return false; } // 总是挂起
void await_suspend(std::coroutine_handle<> coro) noexcept
{
h = coro;
// 异步执行读取
std::async(std::launch::async, [this]{
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file) { std::cerr << "Open file failed\n"; return; }
auto size = file.tellg();
file.seekg(0);
buffer.resize(static_cast <size_t>(size));
file.read(buffer.data(), size);
// 读取完成后恢复协程
h.resume();
});
}
std::vector <char> await_resume() noexcept { return std::move(buffer); }
};
// 2. 协程返回类型
struct task
{
struct promise_type
{
task get_return_object() { return {}; }
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
// 3. 协程函数
task read_file_async(std::string path)
{
auto data = co_await async_read{std::move(path)};
std::cout << "File size: " << data.size() << " bytes\n";
std::string content(data.begin(), data.end());
std::cout << "Content preview: " << content.substr(0, 50) << "...\n";
}
int main()
{
read_file_async("example.txt");
std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待异步完成
return 0;
}
说明
async_read是一个自定义 awaitable,负责异步打开文件并读取内容。read_file_async协程使用co_await等待async_read完成后获取数据。task是最小化的协程返回类型,用来让read_file_async成为协程。
4. 协程在实际项目中的常见场景
| 场景 | 协程作用 |
|---|---|
| 网络 I/O | 通过 co_await 等待网络套接字完成读/写,代码保持同步风格 |
| 生成器 | std::generator 用于遍历大集合(如遍历文件行、数据库结果集) |
| 状态机 | co_yield 产生多阶段过程,适合编写复杂协议解析 |
| 任务调度 | 与事件循环结合,实现微线程、协程池等轻量级并发框架 |
5. 小结
C++20 协程提供了一套语法糖和运行时机制,让我们可以在保持代码可读性的同时,实现高效的异步、惰性或状态机程序。理解其关键字、awaitable 的实现和协程句柄的使用,是上手协程的前提。随着 C++23 引入 std::task、std::promise 的进一步改进,协程将成为 C++ 高性能编程的重要工具。
后续学习建议
- 阅读 RFC 2600(C++20 协程)了解实现细节。
- 结合
boost::asio或libuv等网络库,实践异步网络编程。 - 探索协程与
std::future的混合使用,构建更复杂的并发框架。