co_routine协程入门
自学协程笔记,旨在用自己能接受的简单的语言一步一步详细的认知协程,了解运转流程。学知有限,内容可能有误,欢迎指出。
更专业的请参考BennyHuo的视频和博客。
什么是协程
普通函数:像一次性说完一整段话,中间不能停,说完了就结束。
协程 :像打电话时"你等一下,我查个资料,别挂",查完继续聊。可以暂停 ,稍后恢复 ,能多次这样操作。
下面给出协程所需的结构类型大概浏览,不需要你现在知道什么意思。
cpp
struct Task {
struct promise_type {
// 创建协程时调用
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
// 初始挂起点(true=立即暂停,false=立即执行)
std::suspend_always initial_suspend() { return {}; }
// 最终挂起点(true=暂停,false=不暂停)
std::suspend_always final_suspend() noexcept { return {}; }
// 处理 co_return 的值
void return_void() {}
// 处理异常
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle;
Task(std::coroutine_handle<promise_type> h) : handle(h) {}
~Task() { if (handle) handle.destroy(); }
// 恢复协程
void resume() {
if (handle && !handle.done()) {
handle.resume();
}
}
};
/*
我们这里折叠简化一下
*/
struct Task {
struct promise_type {
Task get_return_object();
std::suspend_always initial_suspend();
std::suspend_always final_suspend();
void return_void() ;
void unhandled_exception();
};
std::coroutine_handle<promise_type> handle;
...
};
经过折叠之后的结构很清晰,我们可以看到外围的Task结构体和内部的promise_type结构体。外部的结构体可以随意取名,内部的promise_type是C++语法要求。
当然我们也可以分开写,随便定义内部结构体的名字,只需要Task使用别名promise_type代替那个结构体。如下:
cpp
struct 随便取一个名字 {
Task get_return_object();
std::suspend_always initial_suspend();
std::suspend_always final_suspend();
void return_void() ;
void unhandled_exception();
};
struct Task {
// 一定要内部有promise_type
using promise_type = 随便取一个名字;
std::coroutine_handle<promise_type> handle;
...
};
上述结构只是协程需要的一个返回类型,但并不是协程。真正的协程函数如下:
cpp
Task myCoroutine() {
std::cout << "协程开始\n";
co_await std::suspend_always{}; // 暂停
std::cout << "协程恢复\n";
co_await std::suspend_always{}; // 再暂停
std::cout << "协程结束\n";
}
如何判断一个函数是协程呢?就是函数内一定要有co_await/co_yield/co_return其中之一一个关键字。
然后协程呢返回类型一定是我们上面定义的Task类型。为什么一定要有这么一个Task类型呢?可以想象,一个可以自由暂停和恢复的函数必然要有复杂的控制,如果没有一个可以告诉我们何时恢复,如何恢复的信息,必然是不可取的。
上面的只是给大家一个概览,
-
协程需要一个结构体Task(可以自定义名字)
-
Task内部需要有满足C++要求的promise_type.
-
协程基本结构,返回类型是Task,
-
协程执行体内需要有特殊的关键字。
好,这就是协程的样子,那我们就开始入门吧。
promise_type
作为协程的返回类型Task内部的结构体promise_type,同时也是C++语法要求的存在,必然起着十分重要的作用。他对协程的启动,挂起,结束等起着很重要的作用。
我们来看他的结构,根据C++语法要求,必需要实现下面四个函数。
cpp
struct promise_type {
Task get_return_object();
std::suspend_always initial_suspend();
std::suspend_always final_suspend();
void unhandled_exception();
};
- get_return_object
cpp
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
这个函数用于外部的Task获取指向协程帧的handle。他会在协程创建的时候,自动调用。Task的成员函数,协程的句柄handle也是自动根据这个生成的.
借助AI给出一个较为清晰的流程图
bash
// 编译器生成的伪代码
Task my_coroutine() {
// 1. 分配协程帧(包含 promise 对象)
coroutine_frame* frame = operator new(sizeof(coroutine_frame));
// 2. 构造 promise 对象(在协程帧内)
new (&frame->promise) Task::promise_type();
// 3. 获取协程帧的句柄(这里才是句柄的"来源")
// 句柄本质上是指向 frame 的指针包装
auto handle = std::coroutine_handle<Task::promise_type>::from_address(frame);
// 4. 调用 get_return_object(),传入 handle
// 注意:from_promise(*this) 只是从 promise 反推回 handle
Task return_object = frame->promise.get_return_object();
return return_object;
}
协程帧 (堆内存)
↓
promise 对象 (在协程帧内)
↓
from_promise(*this) 获取 handle
↓
get_return_object() 返回 Task
↓
Task 构造函数接收 handle
↓
task 对象存储 handle
std::coroutine_handle 到底是什么呢?用一个比喻来说,他就是协程的"遥控器"。手里拿着他,你可以的对协程进行恢复(resume()),销毁(destroy()),检查是否结束(done()),访问我们的promise_type(promise())。
get_return_object可以从当前的promise_type来创建出一个handle,指向真正的协程。promise_type是协程内部和协程外部的核心桥梁。
bash
外部世界
(用户代码)
▲
│ ① 通过 Handle 访问
│
┌────────┴────────┐
│ Promise │ ◄── 桥梁
│ (行为规范) │
└────────┬────────┘
│ ② 控制协程行为
│
▼
协程内部
(协程体代码)
- initial_suspend
cpp
std::suspend_always initial_suspend() { return {}; }
这个函数在协程创建开始的时候调用,根据这个函数的返回值,决定协程开始的时候挂不挂起,也就是一开始到底执不执行。
这时候我们注意到了std::suspend_always 这个结构。除了他之外,还有一个std::suspend_never结构。他们也是C++提供给我们的结构体,同时满足一定的结构。
下面是两个结构体的结构:
cpp
struct suspend_always
{
constexpr bool await_ready() const noexcept { return false; }
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
struct suspend_never
{
constexpr bool await_ready() const noexcept { return true; }
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
我们发现这两个结构体几乎一模一样,唯一不同的是await_ready的返回值。一个是true,一个是false.
满足这两个结构体的结构的类型我们叫做等待体(我们之后也可以自定义等待体),顾名思义,他是用来决定协程是否等待的。
根据我们前面说的,执行到initial_suspend的时候,我们返回了一个等待体。然后协程根据等待体的await_read决定是否挂起。如果await_ready返回true,我们可以简单的想象:**:你ready好了吗?:true!**也就是准备好了(ง •̀_•́)ง,那么就会直接返回继续执行协程,也就是协程不会挂起。
如果await_ready返回false,那就是**:你ready好了吗?:false.** 那么协程没有准备好就会转而执行下面的另一个函数await_suspend,执行完毕之后,协程就开始挂起。然后控制权限返回给调用者。
等协程恢复,await_resume就会被函数调用。
给出下面的示意代码:
cpp
initial_suspend()->awaiter(等待体)
if (awaiter.await_ready() == true){
协程继续,执行协程函数体内的代码
}else if (awaiter.await_ready() == false){
await_suspend();
return返回给调用者。
}
- final_suspend
cpp
std::suspend_always final_suspend() noexcept { return {}; }
这个函数是在整个协程结束之后调用的,同样也返回一个等待体,用于告诉协程结束后是否需要挂起。一般我们使用std::suspend_always,表示结束后总是挂起。挂起的好处是可以让外部的Task去进行手动的清理资源,防止悬空引用。否则可以想象,我们外部的Task还持有协程的句柄,但是协程结束后直接清理资源了,那我们Task手里的handle不就是悬空的吗,如果使用直接出错。
- unhandled_exception
cpp
void unhandled_exception() { std::terminate(); }
这个函数用于处理异常。我们可以选择不同的方式进行处理。这里我们使用了直接结束程序。当然我们也可以在我们的promise_type内部创建一个变量,然后unhandled_exception用于将异常保存在这个变量上,也是一种常见的做法。示意:
cpp
struct promise_type{
std::exception_ptr e;
void unhandled_exception() noexcept {
e = std::current_exception();
}
...
};
初次尝试协程
ok啊,讲完了基本的概念,我们可以先尝试一下协程的效果。我们用AI写了一个简单的例子:
cpp
#include <coroutine>
#include <iostream>
#include <thread>
// 1. 定义返回类型(包含 promise_type)
struct Task {
struct promise_type {
Task get_return_object() {
return Task{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::coroutine_handle<promise_type> handle;
Task(std::coroutine_handle<promise_type> h) : handle(h) {}
~Task() { if (handle) handle.destroy(); }
};
Task myCoroutine() {
std::cout << "协程开始\n";
co_await std::suspend_always{};
std::cout << "协程恢复\n";
co_await std::suspend_always{};
std::cout << "协程结束\n";
}
int main() {
Task task = myCoroutine();
task.handle.resume(); // 输出:协程开始
// 遇到 co_await 暂停
task.handle.resume(); // 输出:协程恢复
// 再遇到 co_await 暂停
task.handle.resume(); // 输出:协程结束
return 0;
}
Task就是我们定义的协程返回类型,内部有promise_type.
当主程序main执行Task task = myCoroutine(); 就算是真正的创建了协程。根据我们上面的介绍,这时候就要执行promise_type的initial_suspend函数。检测到返回值std::suspend_always所以直接挂起,协程刚开始不运行。
这时候控制权限交还给我们的调用者,也就是main,这时候继续往下走,假如此时main中没有其他的代码操作,直接就结束了。因为协程挂起,mian往下走直接运行结束。
但是我们此时main执行了task.handle.resume();,何意味呢?
cpp
---->task.handle
返回Task内部的std::coroutine_handle<promise_type> handle;指向真正的协程帧
这个成员是协程创建的时候,调用协程创建好的promise对象内部的get_return_object传入给Task
---->task.handle.resume()
使用拿到的handle遥控器执行resume()恢复协程运行。
此时协程继续运行进入到函数体内部执行输出"协程开始\n"。然后运行到co_await std::suspend_always{};
前面说过co_await/co_return/co_yield都是标志协程的关键字,他的作用是根据后面的等待体的内容,判断协程接下来的行为。
我们知道std::suspend_always意味着挂起(await_ready()==false)。所以执行到co_await std::suspend_always{}这里的时候协程又挂起了。控制权返回给main.如果main没有别的操作,依然如刚开始假设那样直接结束。
而我们这里再次执行了task.handle.resume();,那么协程恢复运行再次继续执行输出"协程恢复\n"。
下面也同理,运行到co_await std::suspend_always{};,协程挂起,回到main,task.handle.resume();恢复协程,执行输出"协程结束\n"。
最后回到main,main后面没有其他的操作了,于是程序结束。需要注意的是,当离开作用域的时候,这个Task执行析构函数
cpp
~Task() { if (handle) handle.destroy(); }
他会检测指向协程的句柄handle是否存在,如果存在那就清理掉。这和我们上面final_suspend连上了,final_suspend我们返回std::suspend_never挂起,handle不会被清理。因此交给我们这里Task管理。
最终结果
bash
协程开始
协程恢复
协程结束
promise_type::final_suspend返回值导致的生命周期问题
上面提到了final_suspend和Task析构的关系。我们这里就举个反例。
假如我们这个协程是需要返回值,那么我们需要在promise_type内部保存这个值,同时我们的final_suspend不再是前面提到的返回std::suspend_always ,而是std::suspend_never.
cpp
struct promise_type {
int value;
Task get_return_object() {
return Task{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(); }
};
那我外部如何获取这个值呢?我们可以使用task.handle.promise().value拿到这个value.因为这个handle可以通过promise()拿到内部promise_type的信息,然后取value即可。
我们测试如下
cpp
Task myCoroutine() {
std::cout << "协程开始\n";
co_return 5;
}
int main(){
Task task = myCoroutine();
task.handle.resume();
std::cout << task.handle.promise().value << std::endl;
}
我们这个测试需要co_return 返回5,但是这个5怎么跟value联系上,让value存储起来呢?那我们就要实现一个函数,让协程支持co_return.(同理co_yield也需要实现类似函数,这里暂时不考虑)
cpp
struct promise_type {
int value;
Task get_return_object() {
return Task{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(int v){
value = v;
}
};
这里需要实现一个 return_value 的函数,名字是C++标准要求不必纠结。当我们执行co_return 5 的时候,调用return_value(5),将value设置为了5.
好的万事具备我们来考虑我们的测试。刚开始根据initial_suspend我们协程会挂起,然后我们的main中直接resume协程,协程开始运行直接输出"协程开始\n",然后直接co_return 5.然后协程结束,由于final_suspend返回std::suspend_never的缘故,协程不会挂起,直接清理资源(相当于handle.destroy())。回到main,之后,我们想要输出协程得到的value,于是我们 std::cout << task.handle.promise().value << std::endl。
但是这样的话就坏了!刚才提到协程结束,handle.destory()了,也就是task内部的handle是悬空的!这时候使用handle.promise()就会导致段错误!
因此协程中我们一般上promise_type::final_suspend返回std::suspend_always会更加安全。因为handle的生命周期会和外部的Task同步,当Task析构的时候,才会清理掉handle.
但也不是说fianl_suspend永远都要返回std::suspend_always,具体问题具体分析。
自定义Awaiter
前面提到了,co_await可以后面可以跟Awaiter,是一个普通的c++对象,只要实现三个特定函数,比如C++预提供std::suspend_never;std::suspend_always.这里我们可以自定义实现一个Awaiter.
cpp
struct awaiter{
bool await_ready() const noexcept {
// 返回 true → 不暂停,直接继续执行
// 返回 false → 需要暂停,进入 await_suspend
}
// ② 暂停时做什么?
void await_suspend(std::coroutine_handle<> handle) const noexcept {
// 协程即将暂停,这里可以:
// - 保存 handle 以便后续恢复
// - 启动异步操作
// - 调度到其他线程
// - 传递给回调函数
}
// ③ 恢复时做什么?返回值是什么?
T await_resume() const noexcept {
// 协程恢复时调用
// 返回值作为 co_await 表达式的结果
// 可以是 void,也可以是任意类型
}
};
我们这里给出一个使用awaiter的示例:
cpp
#include <chrono>
#include <coroutine>
#include <cstddef>
#include <exception>
#include <future>
#include <iostream>
#include <thread>
struct Task {
struct promise_type {
Task get_return_object() {
return Task{
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::coroutine_handle<promise_type> handle;
Task(std::coroutine_handle<promise_type> h) : handle(h) { }
~Task() {
if (handle) {
handle.destroy();
}
}
};
struct awaiter {
std::shared_future<std::optional<int>> fut;
bool await_ready() noexcept {
return false; // 挂起->执行await_suspend
}
void await_suspend(std::coroutine_handle<> handle) noexcept {
std::cout << "异步任务开始" << std::endl;
auto promise_ptr = std::make_shared<std::promise<std::optional<int>>>();
fut = promise_ptr->get_future();
std::thread([handle, promise_ptr]() {
std::this_thread::sleep_for(std::chrono::seconds(5));
try {
promise_ptr->set_value(999);
handle.resume();
} catch (...) {
promise_ptr->set_value(std::nullopt);
promise_ptr->set_exception(std::current_exception());
}
}).detach();
}
int await_resume() noexcept {
return fut.get().value();
}
};
Task myCoroutine() {
std::cout << "协程开始\n";
int value = co_await awaiter{};
std::cout << "value:" << value << std::endl;
}
int main() {
Task task = myCoroutine();
task.handle.resume();
std::this_thread::sleep_for(std::chrono::seconds(10));
std::cout << "main exit" << std::endl;
return 0;
}
Task我们就还复用之前使用的,一就是协程开始先挂起,等我们在main中手动调用task.handle.resume()恢复协程.我们可以知道,协程一恢复,立马输出"协程开始".运行到co_await的时候,根据awaiter的await_ready返回false,然后执行await_suspend函数,之后协程挂起。
简单介绍下await_suspend内部的逻辑。我们在awaiter结构体存储了一个成员变量std::shared_future
fut ,用于同步等待获取异步任务的结果。然后await_suspend我们首先在堆上创建一个promise,将fut和promise关联上。在堆上创建一个promise,是因为我们整个操作是异步的,也就是注册完异步任务直接不阻塞await_suspend操作完协程立马挂起,栈上的变量都会被清除。我们用智能指针创建堆上的promise,可以防止悬空引用。
然后使用线程启动一个异步任务模拟耗时任务,我们把线程分离掉,不去考虑他的析构。我们传给线程的这个任务是一个lambda函数,捕获了当前的协程handle以及promise_ptr. 在线程内部 sleep 5s后,获得结果(prmise_ptr->set_value(999))后恢复协程(handle.resume()).
协程挂起后,恢复调度到main,这时候我们的main开始sleep 10s防止主程序退出。待到5s之后,线程睡眠结束,设置value之后,handle.resume()恢复协程。这时候await_resume()返回值给协程的myCoroutine的value,然后我们打印出value的值。再5s之后main结束。
bash
❯ ./co_routine
协程开始
异步任务开始
value:999
main exit
co_yield和co_return的使用
co_yield
co_yield效果是挂起协程,并返回一个值给调用者。实际上,co_yield就是语言为co_await实现的一个小马甲,更加的方便使用。我们要是想要支持co_yield需要在promise_type定义函数yield_value.
那么协程使用co_yield的时候原理:
cpp
co_yield value;
// 等价于:
co_await promise.yield_value(value);
实现:
cpp
struct Task {
struct promise_type {
int value;
Task get_return_object() {
return Task{
std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() {
return {};
}
std::suspend_always final_suspend() noexcept {
return {};
}
std::suspend_always yield_value(int value){
this->value = value;
return {};
}
void unhandled_exception() {
std::terminate();
}
};
std::coroutine_handle<promise_type> handle;
Task(std::coroutine_handle<promise_type> h) : handle(h) { }
~Task() {
if (handle) {
handle.destroy();
}
}
};
我们在promise_type内部定义了一个成员value,然后实现了一个函数yield_value,当协程调用co_yield value .的时候就会把value传入yield_value函数,然后赋值给promise_type内部的value.然后返回std::suspend_always协程挂起。
我们做个小测试。
cpp
Task myCoroutine() {
std::cout << "协程开始\n";
int i = 5;
while (i--) {
co_yield i;
}
}
int main() {
Task task = myCoroutine();
task.handle.resume(); // 启动协程
for(;;){
if (task.handle.done()) {
break;
}
std::cout << task.handle.promise().value << std::endl;
task.handle.resume();
}
return 0;
}
我们在协程启动之后,循环中co_yield i.第一次co_yield 4,然后协程挂起,恢复到主线程。主线程也是一个循环检测协程的状态,如果协程还没有结束,就把promise_type内部的value输出,然后再次启动协程。
协程启动后,循环继续,co_yield 3,同理,恢复主线程,输出3,然后恢复协程.....如此往复,直到输出0之后,协程恢复,检查到i--为0,循环结束,协程就退出,那么handle.done()==true .这时候主线程检测到task.handle.done(),就break退出,整个程序退出。
bash
❯ /home/vivek/Codes/some_implementations/co_routine/bin/debug/co_routine
协程开始
4
3
2
1
0
co_return
同co_yield一样,也需要在promise_type实现一个函数return_value.区别在于co_return只能使用一次,就结束协程返回最终结果
cpp
void return_value(int v){
value = v;
}
当我们使用co_return的时候
cpp
{
...
co_return 5;
}
// -> handle.promise().return_value(5);
然后结束协程。基本和co_yield差不多,不多做赘述。
还有一点,co_return还有可能返回的是void类型,这个比较特殊,所以需要特殊的函数进行定义
cpp
void return_void(){
...
}
对返回体的co_await
我们知道,co_await的目标是返回体。但是比如一个协程调用另一个协程,想要对另一个协程进行co_await.有两个方法,一个是在Task的promise_type内部实现一个await_transform 函数,另一个就是在Task重载co_await运算符两种做法:
cpp
// 重载co_await
struct Task {
struct promise_type {
int value;
Task get_return_object() {
return Task{
std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() {
return {};
}
std::suspend_always final_suspend() noexcept {
return {};
}
void return_value(int v) {
value = v;
}
void unhandled_exception() { }
};
std::coroutine_handle<promise_type> h;
Task(std::coroutine_handle<promise_type> h) : h(h) { }
~Task() {
if (h) {
h.destroy();
}
}
struct awaiter {
Task &task;
bool await_ready() {
return task.h.done();
}
void await_suspend(std::coroutine_handle<promise_type> caller) {
// 在新线程中等待 task 完成
std::thread([caller, this]() {
while (!task.h.done()) {
task.h.resume();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
caller.resume(); // 恢复调用者
}).detach();
}
int await_resume() {
return task.h.promise().value;
}
};
};
auto operator co_await(Task &&task) {
return Task::awaiter{task};
}
// 实现await_transform
struct Task {
struct promise_type {
int value;
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(int v) { value = v; }
void unhandled_exception() {}
auto await_transform(Task&other){
struct awaiter {
Task& task;
bool await_ready() { return task.h.done(); }
void await_suspend(std::coroutine_handle<> caller) {
// 在新线程中等待 task 完成
std::thread([caller, this]() {
while (!task.h.done()) {
task.h.resume();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
caller.resume(); // 恢复调用者
}).detach();
}
int await_resume() { return task.h.promise().value; }
};
return awaiter{other};
}
};
std::coroutine_handle<promise_type> h;
Task(std::coroutine_handle<promise_type> h) : h(h) {}
~Task() { if (h) h.destroy(); }
};
}
测试
cpp
// 协程A:模拟一个耗时计算
Task slow_calculation() {
for (int i = 1; i <= 5; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
co_return 42;
}
// 协程B:模拟另一个耗时计算
Task another_calculation(int input) {
std::this_thread::sleep_for(std::chrono::seconds(1));
int result = input * 2;
co_return result;
}
// 主协程:等待其他协程
Task main_coroutine() {
int result1 = co_await slow_calculation();
int result2 = co_await another_calculation(result1);
int final_result = result1 + result2;
co_return final_result;
}
using namespace std::chrono_literals;
int main(){
Task task = main_coroutine();
task.h.resume();
std::this_thread::sleep_for(5s);
if (task.done()) {
int result = task.h.promise().value;
std::cout << "协程执行完成,最终结果: " << result << std::endl;
} else {
std::cout << "协程未完成(可能需要更长的等待时间)" << std::endl;
}
}
逻辑简单来说就是main_coroutine主协程S启动,首先是co_await第一个协程A,第一个协程由于重载了co_await,他会根据返回的await判断是否挂起。在我们的例子中是挂起走到了await_suspend中,我们在这里面新开了一个线程,线程一直检测,如果task也就是这个协程A一直没有done,那我们就循环检测,然后让协程A一直resume().
也许你有疑问,为什么要循环resume()?在我们这个例子中,协程A与第二个协程B只会挂起一次,只要resume()一次之后,协程就一直执行完了,没有其他的co_await等挂起点。但是实际工作中可能有好多的挂起点,多处都需要唤醒。所以我们这里采用循环唤醒。如果检测到task.h.done(),也就是协程结束了,那么就caller.resume(),这里的caller是await_suspend传进来的handle,实际上就是调用者协程S的handle.
主协程S醒来之后,继续往下执行开始int result2 = co_await another_calculation(result1);,协程B就开始挂起,然后一系列流程就和协程A一样了。待协程B也结束之后,我们就可以计算final_result,然后co_return返回。
我们的main中,显示sleep了5s,防止主程序退出,5s之后检查主协程S是否完毕,是就输出结果。
bash
❯ ./co_routine
协程执行完成,最终结果: 126
至此,我们协程的常用操作,结构都已经基本掌握!
简单的序列生成器
简单序列器
简单说明一下,就是可以自动生成指定的数字序列比如生成0-1-2-...,又或者是生成一个斐波那契数列等等。
下面给出基本的结构,先不附加太多东西,我们之后一点一点的添加。
cpp
template <typename T>
struct Generator {
struct promise_type {
T value;
bool has_value = false;
std::exception_ptr e;
Generator get_return_object() noexcept {
return Generator{
std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend() noexcept {
return {};
}
std::suspend_always final_suspend() noexcept {
return {};
}
std::suspend_always yield_value(T value) noexcept {
this->value = value;
has_value = true;
return {};
}
std::suspend_always return_value(T value) noexcept {
this->value = value;
return {};
}
T get_value() const noexcept {
return value;
}
std::suspend_always await_transform(T value) {
this->value = value;
return {};
};
void unhandled_exception() noexcept {
e = std::current_exception();
}
void set_value(T v) noexcept {
value = v;
}
};
int get_value() const noexcept {
return handle.promise().value;
}
Generator<int> operator co_await() {
return handle;
}
Generator(Generator const &) = delete;
Generator &operator=(Generator const &) = delete;
Generator(Generator &&other) noexcept
: handle(std::exchange(other.handle, {})) { };
Generator &operator=(Generator &&other) noexcept {
handle = std::exchange(other.handle, {});
return *this;
}
std::coroutine_handle<promise_type> handle;
std::coroutine_handle<promise_type> get_handle() noexcept {
return handle;
}
Generator(std::coroutine_handle<promise_type> h) : handle(h) { }
bool has_handle = true;
~Generator() {
if (handle && handle.done()) {
std::cout << "destory" << std::endl;
handle.destroy();
}
}
bool has_next() noexcept {
if (handle.done()) {
return false;
}
if (!handle.promise().has_value) {
handle.resume();
}
return !handle.done();
}
T next() const noexcept {
handle.promise().has_value = false;
return handle.promise().value;
}
};
刚看上去,代码很多很乱,实际上只要抓住我们协程最主要的那几个结构即可。首先是外层的Task,然后定义内部的promise_type结构体。
与前面不同的是我们还在Task定义了许多便利的成员函数,比如get_value(),set_value(),get_handle()等。之前我们获取值的时候都是直接task.handle.promise().value,现在我们定义函数之后,直接task.get_value()即可(虽然之后我们可能没怎么用到。。。)。
还有一个就是结构体使用了模板,这样就不局限于单一的int或者float,而是更加通用了。
了解了这些不同我们就来到了最重要has_next和next函数了。
Task内部有个成员函数 has_value 标志是否有值。我们外部使用的时候调用has_next 成员函数用来检测是否有值,有值的话,就可以调用next获取值。
cpp
Generator sequence() {
int i = 0;
while (i < 5) {
co_await i++;
}
}
int main() {
auto generator = returns_generator();
for (int i = 0; i < 15; ++i) {
if (generator.has_next()){
std::cout << generator.next() << std::endl;
continue;
}
break;
}
return 0;
}
bash
1
2
3
4
destory
简单说一下流程,第一步main创建协程之后(auto generator = returns_generator() ),默认has_value = false,协程挂起回到main函数。main循环中,首先判断是否有值(has_next() ),检测到handle没有done,同时!handle.promise().has_value,那么协程就恢复了。协程恢复之后co_await i++ 调用函数await_transform 设置task内部的value为i,然后继续挂起(await_transform返回std::suspend_always)。
这时候has_next()继续进行返回true,main接着就打印出generator.next().如此循环往复,就导致了上述结果。
bash
初始状态 → has_value=false, done=false
↓
has_next() → resume() → 执行到 co_await
↓
挂起状态 → has_value=true, done=false
↓
next() → 读取值, has_value=false
↓
重复以上过程
↓
最终状态 → done=true
我们这个使用的co_await,还需要重载co_await或者实现await_transform函数,每次产生一个值,其实使用co_yield更加方便,只需要实现yield_value函数,所以更加推荐使用yield_value.
斐波那契数列
有了上面的基础,使用协程实现一个斐波那契数列十分的容易:
cpp
Generator<int> fibonacci() {
co_yield 0;
co_yield 1;
int a = 0;
int b = 1;
while (true) {
co_yield a + b;
b = a + b;
a = b - a;
}
}
int main() {
auto g = fibonacci();
int k = 10;
while (k-- && g.has_next()) {
std::cout << g.next() << std::endl;
}
}
结果如下:
bash
./co_routine
0
1
1
2
3
5
8
13
21
34
函数式编程
我们能不能从一个给定的数组或者给定的序列构建这个序列器,然后一个一个来取用呢?当然可以
cpp
Generator static from_array(T array[], int n) {
for (int i = 0; i < n; ++i) {
co_yield array[i];
}
}
然后我们就可以这样使用
cpp
int array[] = {1, 2, 3, 4};
auto generator = Generator<int>::from_array(array, 4);
while(....){
std::cout << ...
}
但是不够优雅,还需要手动传入大小,如果传入vector或者list等,就可以直接使用范围for直接遍历,无需手动传入长度。
cpp
Generator static from_array(std::vector<T> array) {
for (auto&p:array) {
co_yield p;
}
}
但是如果创建还是需要类似如下操作
cpp
Generator::from_array<int>({1,2,3,4});
std::vector<int>vec{1,2,3,4};
Generator::from_array<int>(vec);
如何去掉这个繁琐的{}或者容器呢?我们可以使用变参模板.
cpp
template<typename T>
struct Generator {
...
template<typename ...TArgs>
Generator static from(TArgs ...args) {
(co_yield args, ...);
}
}
这里的 (co_yield args, ...) 是折叠表达式,简而言之就是co_yield args是一种模式,表示什么操作。***++...++***则意味着每个参数都要按照前面的模式进行。等价一下代码:
cpp
template<typename ...TArgs>
Generator static from(TArgs ...args) {
co_yield arg1,co_yield arg2,co_yield 3,.....;
}
简单使用一下吧:
cpp
int main() {
auto p = Generator<int>::from_array(1,2,3,4);
while(p.has_next()){
std::cout << p.next() << std::endl;
}
}
这里创建协程就不需要单独定义一个协程函数了,而是直接根据静态成员函数直接创建,输出结果:
bash
1
2
3
4
destory
更多的函数式操作
我们可以添加更多的操作,比如map函数。map操作在其他的语言很常见,就是对传入的所有参数执行同一个变换操作。
cpp
template <typename F>
Generator<std::invoke_result_t<F, T>> map(F &&f) {
while (has_next()) {
co_yield f(next());
}
}
我们给这个协程函数传入一个函数f,然后对每一个值进行f操作返回。***std::invoke_result_t<F, T>***可以根据传入的函数类型和操作的参数类型推导出返回的结果类型。
使用案例(对每个值进行*2操作,将每一个值转成std::string )
cpp
int main() {
auto p = Generator<int>::from_array(1,2,3,4).map([](auto i){
return i * 2;
}).map([](auto i){
return std::to_string(i);
});
while(p.has_next()) {
std::cout << p.next() << std::endl;
}
}
我们协程p的操作根据给出的指定序列对每一个值进行*2然后再进行一次转成std::string的操作,最后循环输出。结果如下:
bash
./co_routine
2
4
6
8
destory
有了map操作,其他的操作也就很容易理解和实现了,这里直接给出,你应该能够理解他的意思了!
cpp
// 扁平化
/* 输入: [A, B, C]
经过 flat_map:
A → [a1, a2]
B → [b1]
C → [c1, c2, c3]
结果: [a1, a2, b1, c1, c2, c3] // 一维数组
*/
template <typename F>
std::invoke_result_t<F, T> flat_map(F &&f) {
while (has_next()) {
auto generator = f(next());
while (generator.has_next()) {
co_yield generator.next();
}
}
}
// 这返回的不是一个协程!操作是每一个数据都进行f操作
template <typename F>
void for_each(F &&f) {
while (has_next()) {
f(next());
}
}
// 多个值结合为一个
template <typename R, typename F>
R fold(R initial, F &&f) {
R cc = initial;
while (has_next()) {
cc = f(cc, next());
}
return cc;
}
// 求和
T sum() {
T sum = 0;
while (has_next()) {
sum += next();
}
return sum;
}
// 筛选
template <typename F>
Generator filter(F &&f) {
while (has_next()) {
T value = next();
if (f(value)) {
co_yield value;
}
}
}
// 取前n个
Generator take(int n) {
int i = 0;
while (has_next() && i < n) {
co_yield next();
i++;
}
}
template <typename Func>
Generator take_while(Func &&f) {
while (has_next() && f(next())) {
co_yield next();
}
}
测试1
cpp
int main() {
auto p = Generator<int>::from_array(1, 2, 3, 4)
.map([](auto i) { return i * 2; }) // 2,4,6,8
.filter([](auto i) { return i % 2 == 0; }) // 2,4,6,8
.take(3) // 2,4,6
.take_while([](auto i) { return i < 10; }); // 2,4,6
while (p.has_next()) {
std::cout << p.next() << std::endl;
}
}
bash
./co_routine
2
4
6
destory
测试2
cpp
int main() {
auto p = Generator<int>::from_array(1, 2, 3, 4)
.flat_map([](auto i) -> Generator<int> {
for (int j = 0; j < i; ++j) {
co_yield j; // 这里每个i值,生成多个又会生成多个协程
}
})
.take(6);
while (p.has_next()) {
std::cout << p.next() << std::endl;
}
}
bash
./co_routine
0
destory
0
1
destory
0
1
2
destory
destory
这里最后得到一个协程p,为什么会有这么多的destory呢?其实根据函数的返回值我们都知道,中间的什么take,flat_map其实都会生成协程,不过这个协程是临时的。最后的take生成的协程赋值给了p.
前面的协程因为是临时的协程对象,按道理说返回临时协程之后就应该析构了,输出destroy.但是实际上并没有输出。为什么?因为final_suspend都是返回std::suspend_always结束后协程挂起,然后临时的协程准备析构,检查
cpp
~Generator() {
if (handle && handle.done()) {
std::cout << "destory" << std::endl;
handle.destroy();
}
}
发现handle还没有done。为什么没有done?比如,刚开始from_array(1,2,3,4),一开始只是返回了这个协程,都没有co_yield去消耗,怎么可能done呢?于是不析构。
那么为什么结果中这么多destroy呢?这是因为flat_map而原因,他会根据每个i生成i个临时的协程,每个协程都要co_yield值,当最后消耗完毕之后handle.done就为true,这些临时协程就结束了,于是输出destroy.
最后p(从take得到的协程)因为也完美析构了,也输出了destroy.但是协程调用链条take之前的临时对象都没有析构了(不包括flat_map内部又每次生成的i个协程)。
生命周期问题
cpp
template <typename... Args>
static Task from_array(Args &&...args) {
(co_yield args, ...);
}
第一种实现方式(直接在临时协程上进行操作)
cpp
template <typename F>
Task<std::invoke_result_t<F, T>> map(F &&f) {
while (has_next()) {
co_yield f(next());
}
}
auto p = Task<int>::from_array(1, 2, 3, 4).map([](auto i) {
return std::to_string(i);
});
while (p.has_next()) {
std::cout << p.next() << std::endl;
}
输出
cpp
1
2
3
4
destory
bash
1. from_array 创建 A(协程帧在堆上)
2. map 创建 B
3. 临时对象 A 析构(在表达式结束时):
- A.handle.done() == false(还没执行过)
- 条件不满足,不销毁 A 的帧! ← 内存泄漏
4. 外部 while 循环驱动 B 执行:
- B 内部调用 this->has_next(),this 指向已销毁的 A
- 但 A 的协程帧还在堆上(未销毁)
- 正常工作
5. B 完成,B 的析构函数执行:
- B.handle.done() == true → 输出 "destory" → 销毁 B 的帧
6. A 的协程帧呢?没人销毁!永远泄漏了
第二种实现方式(转移所有权)
cpp
template <typename F>
Task<std::invoke_result_t<F, T>> map(F &&f) {
auto up_stream = std::move(*this); // auto推导为Task<int>
while (up_stream.has_next()) {
co_yield f(up_stream.next());
}
}
输出
cpp
1
2
3
4
destory
destory
bash
1. from_array 创建 A(协程帧在堆上)
2. map 创建 B
3. map 函数体执行(在 B 的上下文中):
- auto upstream = std::move(*this); // upstream 持有 A
- 消费完 A,A 变成 done()
- 循环结束
4. B 返回,upstream 是 B 的局部变量
5. 外部 while 循环驱动 B 输出值
6. B 完成,B 的析构函数执行:
- B.handle.done() == true → 输出 "destory" → 销毁 B 的帧
7. B 析构时,upstream 也被析构(B 的局部变量)
- upstream 是 Task 对象,持有 A 的 handle
- A.handle.done() == true → 输出 "destory" → 销毁 A 的帧
结论
按照第一种方式,实际上启动了两个不同的协程A/B.不过协程B也就是map启动的协程因为依赖协程A产生值,而A还没有结束,但是这个协程是from_array产生的临时对象,需要被析构,临时对象析构的时候检查A::handle.done()==false,所以不会执行A::handle.destory().而B协程结束后,检查handle.done() == true,所以执行B::handle.destory().
因此,第一种方式只会有一个destory,实际上A的handle泄漏了,因为析构的时候没有destory掉,后续就没有处理了。
而第二种方式,则是协程B直接将协程A的handle转移到了B中,即这里auto up_stream = std::move(*this); 实际这里的up_stream就是Task 类型,就是A被转交给了B.因为B一直存在求值,内部的up_stream也是一直存活的, 也就是实际上协程A还没有走到析构那一步 。当协程B结束的时候,B调用析构函数,执行B::handle.destory(),输出一个 destory,这时候掌管协程A的up_stream也要析构,执行A::handle.destory(),也会输出一个 destory.
实现了这些函数之后,我们可以很方便的进行链式调用(先不考虑协程帧泄漏的问题):
cpp
#include "generator.hpp"
#include <iostream>
#include <string>
#include <thread>
#include <vector>
Generator<int> sequnence() {
int i = 0;
while (i < 4) {
co_yield i++;
}
}
Generator<int> fibonacci() {
co_yield 0;
co_yield 1;
int a = 0, b = 1;
while (true) {
co_yield a + b;
b = a + b;
a = b - a;
}
}
struct out {
template <typename T>
void operator()(T const &value, char separator = ' ') {
std::cout << value << separator;
};
};
struct outln {
template <typename T>
void operator()(T const &value) {
std::cout << value << std::endl;
};
};
static inline outln outln;
static inline out out;
int main(int, char **) {
fibonacci()
.take_while([](auto i) { return i < 20; })
.filter([](auto i) { return i % 2 == 0; })
.map([](auto i) { return i * 2; })
.flat_map([](auto i) -> Generator<int> {
for (int j = 0; j < i; ++j) {
co_yield j;
}
})
.for_each([](auto i) { std::cout << i << std::endl; });
}
看上去十分的炫酷。。。
具体调用流程
bash
每个操作都创建了一个新的协程:
A: fibonacci (无限流)
B: take_while (包装 A)
C: filter (包装 B)
D: map (包装 C)
E: flat_map (包装 D)
F: for_each (普通函数,不是协程)
1. for_each 驱动 E (flat_map)
↓
2. E 从 D 取值
↓
3. D 从 C 取值
↓
4. C 从 B 取值
↓
5. B 从 A 取值
↓
6. A 产生一个值 (0, 1, 1, 2, 3, 5, 8, 13...)
↓
7. 值向上传递,经过转换
↓
8. E 产生一个内层序列 (0,1,2,...,i-1)
↓
9. for_each 消费这些值
测试结果如下:
bash
destory
0
1
2
3
destory
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
destory
destory
destory
destory
destory
根据我们上面生命周期那里,按道理而言应该最后才会输出一个destory,为什么这里输出了这么多的destory,有的穿插在中间?
我们换一个小的数值演示
cpp
// 0 0 1 2 3
fibonacci()
.take_while([](auto i) { return i < 3; })
.filter([](auto i) { return i % 2 == 0; })
.map([](auto i) { return i * 2; })
.flat_map([](auto i) -> Generator<int> {
for (int j = 0; j < i; ++j) {
co_yield j;
}
})
.for_each([](auto i) { std::cout << i << std::endl; });
我们i<3足够小的序列,斐波那契生成0 0 1 2 可以通过take_while.
然后第一个0,通过filter ,然后通过map ,来到flat_map,这时候这个i还是0.
根据上面的flat_map的定义
cpp
template <typename F>
std::invoke_result_t<F, T> flat_map(F &&f) {
while (has_next()) {
auto generator = f(next());
while (generator.has_next()) {
co_yield generator.next();
}
}
}
实际上生成的还是一个协程,也就是执行到flat_map的时候,根据函数f的操作,再次生成好多的协程。
但是我们传入的for循环用来生成指定的序列.
cpp
for (int j = 0; j < i; ++j) {
co_yield j;
}
由于i=0,没有生成协程,那么第一个内层生成器就结束了,销毁了,输出"destory"。接下来回到take_while 和filter ,i0/i1都不满足直接pass.当i2的时候,一路调用下来,到了falt_map此时的i4.根据条件生成了内部值为0 1 2 3 的4个序列器。然后四个分别运行到for_each打印"0 1 2 3",然后结束销毁。再次输出"destory".这时候整个序列器生成完毕,调用链上的所有临时生成的协程一次销毁,共四个(没有for_each),所以再次输出4个"destory"。结果如下:
cpp
destory
0
1
2
3
destory
destory
destory
destory
destory
简单的销毁如下:
cpp
destory ← 第一个内层生成器(i=0,空)
0
1
2
3
destory ← 第二个内层生成器(i=4,完成)
destory ← take_while 协程 A 完成
destory ← filter 协程 B 完成
destory ← map 协程 C 完成
destory ← flat_map 协程 D 完成