1. 什么是协程?
协程是一个可以挂起 (暂停执行)并在稍后恢复的函数。与普通函数不同,协程可以:
- 在挂起时不阻塞线程
- 保留局部状态,恢复后继续执行
- 由开发者明确控制挂起点(
co_await,co_yield,co_return)
C++20通过三个新关键字和一套编译器协议支持无栈协程。
2. 协程的三大关键字
| 关键字 | 作用 | 示例 |
|---|---|---|
co_await |
挂起协程,等待某个操作完成 | co_await async_read(...) |
co_yield |
挂起并返回一个值给调用者,可多次产生值 | co_yield i; |
co_return |
结束协程,并可选返回一个最终值 | co_return result; |
3. 核心机制:Promise与Awaitable协议
协程的编译行为由两个用户定义的类型决定:Promise 和 Awaitable。
3.1 Promise 类型(promise_type)
每个协程关联一个promise_type对象,它定义了协程的生命周期行为。必须在协程的返回类型中嵌套定义。
cpp
struct ReturnObject {
struct promise_type {
// 必须实现的函数
ReturnObject get_return_object(); // 创建协程的返回对象
std::suspend_never initial_suspend(); // 协程开始时是否挂起
std::suspend_never final_suspend() noexcept;// 协程结束时是否挂起
void return_void(); // co_return 无值时调用
void unhandled_exception(); // 异常处理
// 可选函数(使用 co_yield 时需要)
std::suspend_always yield_value(int value);
};
};
调用时机(由编译器自动调用) :
1.调用协程函数 → 创建promise对象 → 调用get_return_object获得返回对象。
2.立即调用initial_suspend:若返回suspend_always则协程初始挂起;否则执行函数体。
3.协程内遇到co_return → 调用return_void或return_value → 调用final_suspend。
4.异常发生时调用unhandled_exception。
例子:
cpp
#include <coroutine>
#include <iostream>
template<typename T>
struct Generator {
struct promise_type {
T current_value;
Generator get_return_object() {
return Generator{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(); }
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
void return_void() {}
};
std::coroutine_handle<promise_type> handle;
Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
// 迭代器风格
bool next() {
if (!handle) return false;
handle.resume();
return !handle.done();
}
T value() const { return handle.promise().current_value; }
};
Generator<int> count_up_to(int max) {
for (int i = 1; i <= max; ++i) {
co_yield i; // 挂起,返回 i
}
}
int main() {
auto gen = count_up_to(5);
while (gen.next()) {
std::cout << gen.value() << ' ';
}
// 输出: 1 2 3 4 5
}
Awaitable 类型(用于 co_await)
co_await表达式期待一个可等待对象(Awaitable),该对象需实现三个函数:
cpp
struct Awaitable {
bool await_ready(); // 是否需要挂起?false表示挂起
void await_suspend(std::coroutine_handle<> h); // 挂起时执行,保存句柄等
T await_resume(); // 恢复后执行,返回结果
};
调用时机:
1.co_await awaitable_obj → 先调用await_ready(),若返回true则不挂起。
2.若挂起,调用await_suspend(handle),协程暂停,控制权返回调用者/调度器。
3.未来某个时刻,通过handle.resume()恢复协程 → 调用await_resume(),其返回值作为整个co_await表达式的值。
例子:
cpp
#include <coroutine>
#include <iostream>
#include <thread>
#include <string>
#include <atomic>
// 一个简易的 awaitable 类型,用于异步读取一行
struct AsyncReadLine {
// 协程句柄(在 await_suspend 中保存)
std::coroutine_handle<> handle_;
// 存放读取结果
std::string result_;
// 用于同步的原子标志,避免重复恢复
std::atomic<bool> resumed_{false};
// await_ready:返回 false 表示需要挂起
bool await_ready() const noexcept {
return false; // 总是挂起,等待输入
}
// await_suspend:传入协程句柄,启动异步操作
void await_suspend(std::coroutine_handle<> h) {
handle_ = h; // 保存句柄,以便后面恢复
// 启动一个线程,模拟异步 IO
std::thread([this] {
// 阻塞读取直到换行符
std::string line;
std::getline(std::cin, line);
// 存储结果
result_ = std::move(line);
// 恢复协程
if (!resumed_.exchange(true)) {
handle_.resume();
}
}).detach(); // 分离线程,使其独立运行
}
// await_resume:协程恢复后,返回读取的结果
std::string await_resume() {
return std::move(result_);
}
};
// 使用协程:等待用户输入一行,然后打印
#include <coroutine>
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 wait_and_print() {
std::cout << "协程:请输入一行文字:";
// co_await 一个 AsyncReadLine 对象
std::string line = co_await AsyncReadLine{};
std::cout << "协程:你输入的是:" << line << std::endl;
}
int main() {
// 启动协程
wait_and_print();
// 主线程在协程挂起期间继续执行其他工作
for (int i = 0; i < 10; ++i) {
std::cout << "主线程:执行其他任务 " << i << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
// 等待一下,防止程序退出时协程还未完成
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "主线程结束" << std::endl;
return 0;
}
总结
协程不是线程,它不能利用多核。实际应用中,协程通常与线程池或事件循环结合:
- 协程负责逻辑编排(挂起/恢复)
- 线程负责执行和真正的并行I/O
参考文章:https://blog.csdn.net/CD200309/article/details/157548774