在 C++20 中,协程(Coroutine)被正式加入标准库,为开发者提供了一种更加优雅地编写异步、延迟计算和生成器等功能的手段。相比传统的回调、Future/Promise 以及第三方库,协程的语法更接近同步代码,降低了错误率并提升了可读性。本文将从协程的基本概念、实现原理到实际使用场景进行全面阐述,并给出完整示例,帮助你快速上手。
1. 协程到底是什么?
协程是一种能暂停和恢复执行的函数。它在执行过程中可以“挂起”(suspend)并返回控制权,待需要继续时再恢复(resume)。在 C++20 中,协程的实现核心是:
- 协程函数:使用
co_await、co_yield、co_return关键字的函数。 - 协程句柄:
std::coroutine_handle对协程进行控制(挂起、恢复、销毁)。 - 协程 Promise:
promise_type结构,负责协程的状态、返回值等。
2. 基本语法示例
下面演示一个最简洁的协程:生成整数序列。
#include <coroutine>
#include <iostream>
struct IntGenerator {
struct promise_type {
int current_value;
std::suspend_always yield_value(int value) {
current_value = value;
return {};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
IntGenerator get_return_object() { return { std::coroutine_handle <promise_type>::from_promise(*this) }; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> handle;
explicit IntGenerator(std::coroutine_handle <promise_type> h) : handle(h) {}
~IntGenerator() { if (handle) handle.destroy(); }
struct Iterator {
std::coroutine_handle <promise_type> h;
bool done = false;
int operator*() const { return h.promise().current_value; }
Iterator& operator++() {
h.resume();
done = h.done();
return *this;
}
};
Iterator begin() { handle.resume(); return {handle, handle.done()}; }
Iterator end() { return {handle, true}; }
};
IntGenerator count(int n) {
for (int i = 0; i < n; ++i)
co_yield i; // 暂停并返回当前值
}
使用方式:
int main() {
for (int x : count(5))
std::cout << x << ' '; // 输出 0 1 2 3 4
}
解释:
co_yield将当前值保存到 promise,并暂停协程。Iterator调用handle.resume()继续执行,直到遇到下一个co_yield或协程结束。co_await、co_return与co_yield类似,只是语义不同。
3. 协程的工作机制
-
编译阶段
编译器将协程函数拆分为若干基本块,并生成一个promise_type结构。协程函数的返回类型必须满足has_return_object,即提供get_return_object()。 -
运行时
- 创建协程句柄
handle并绑定到promise_type。 initial_suspend()决定是否立即挂起;若返回std::suspend_always,协程立即挂起。co_yield/co_await/co_return在执行时调用对应的 promise 方法,并根据返回值决定是否挂起。final_suspend()决定协程结束时是否再次挂起(常见返回std::suspend_always以防资源泄漏)。
- 创建协程句柄
-
资源管理
std::coroutine_handle提供destroy()、resume()等操作,协程结束后需显式销毁。
4. 常见协程模式
4.1 生成器(Generator)
如前例所示,co_yield 用于生成序列。适用于大数据流、懒加载等场景。
4.2 异步 I/O(Async I/O)
结合 co_await 与事件循环框架(如 asio 或自定义 io_context)实现非阻塞 I/O。示例(伪代码):
struct AsyncRead {
struct promise_type {
// ...
std::suspend_always await_suspend(std::coroutine_handle<> h) {
// 注册 I/O 完成回调,完成时 resume 协程
register_read_cb([h]{ h.resume(); });
return {};
}
int await_resume() { return /* 读取的数据 */; }
};
};
AsyncRead read_file(int fd, char* buffer, std::size_t size) {
co_return co_await async_read(fd, buffer, size);
}
4.3 流程控制(State Machines)
使用 co_await 与自定义 Awaitable 对象可实现简洁的状态机,避免深层回调。
4.4 并行/并发(Parallel)
通过 co_spawn(如 boost::asio::spawn)或自定义调度器,将协程作为轻量级任务提交到线程池,利用协程的协作式调度提升并发度。
5. 与传统 Future/Promise 的对比
| 特点 | 协程(Coroutine) | Future/Promise |
|---|---|---|
| 语法 | co_await、co_yield |
future.get() |
| 错误传播 | 通过异常传播 | 需要显式捕获 |
| 资源管理 | 自动销毁 | 需要手动管理 |
| 可读性 | 类似同步代码 | 回调链式较难 |
| 性能 | 轻量级协作式调度 | 线程/任务开销大 |
6. 开发工具与编译器支持
- GCC:自 10.2 起已完整实现 C++20 协程。
- Clang:自 10 起支持协程,推荐使用
-std=c++20。 - MSVC:从 Visual Studio 2019 版本 16.11 开始支持协程。
- 库:
cppcoro、cppcoro::generator、cppcoro::task等第三方实现。 - IDE:CLion、VSCode(C++插件)均已支持协程语法高亮与调试。
7. 示例:异步文件读取 + 生成器
#include <coroutine>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <filesystem>
using namespace std::chrono_literals;
// 简易异步 I/O 伪实现
struct AwaitableRead {
std::string file;
std::string& buffer;
std::size_t offset;
std::size_t size;
AwaitableRead(std::string f, std::string& buf, std::size_t o, std::size_t s)
: file(std::move(f)), buffer(buf), offset(o), size(s) {}
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) const {
// 异步读取(此处用同步模拟)
std::ifstream ifs(file, std::ios::binary);
ifs.seekg(offset);
buffer.resize(size);
ifs.read(&buffer[0], size);
h.resume();
}
std::string await_resume() const { return buffer; }
};
struct AsyncReadFile {
std::string file;
std::coroutine_handle<> handle;
AsyncReadFile(std::string f) : file(std::move(f)) {}
~AsyncReadFile() { if (handle) handle.destroy(); }
struct promise_type {
std::string result;
std::coroutine_handle <promise_type> get_return_object() {
return std::coroutine_handle <promise_type>::from_promise(*this);
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
void return_value(std::string r) { result = std::move(r); }
};
struct Iterator {
std::coroutine_handle <promise_type> h;
std::size_t chunk = 0;
static constexpr std::size_t CHUNK_SIZE = 1024;
Iterator(std::coroutine_handle <promise_type> h_) : h(h_) {}
std::string operator*() {
std::string buffer;
h.promise().return_value(co_await AwaitableRead(h.promise().result, buffer, chunk, CHUNK_SIZE));
return buffer;
}
Iterator& operator++() { ++chunk; return *this; }
bool operator!=(const Iterator& other) const { return h != other.h; }
};
Iterator begin() { handle.resume(); return {handle}; }
Iterator end() { return {handle}; }
};
int main() {
// 假设存在大文件 "large.bin"
for (auto chunk : AsyncReadFile("large.bin")) {
std::cout << "读取到 " << chunk.size() << " 字节\n";
}
}
提示:真实项目中请使用成熟的异步 I/O 库(如
asio、libuv)来实现AwaitableRead,此处仅为演示。
8. 常见坑与调试技巧
- 忘记
return:协程函数若未co_return,编译器会报unreachable错误。 - 资源泄漏:若协程抛异常未被捕获,
promise_type可能未正确销毁;使用try/catch或final_suspend()处理。 - 调试难度:协程内部状态隐藏在生成的状态机里,可通过
-fdump-tree-original等编译器选项查看展开后的代码。 - 调度器选择:默认协程在当前线程恢复,若需多线程请自定义
std::coroutine_traits或使用第三方调度器。
9. 结语
C++20 协程为语言本身注入了强大的异步能力,使得传统的回调地狱、链式 Promise 以及繁琐的线程同步得以简化。只需几行代码即可实现高性能、易读的生成器、异步 I/O 与并发任务。掌握协程将为你在现代 C++ 开发中打开新的可能性——从嵌入式系统到高性能服务器,协程都能发挥其独特优势。希望本文能帮助你快速上手,并在项目中实际运用协程,提升代码质量与开发效率。祝编码愉快!