文章目录
什么是协程
我们在学习编程的过程中,逐渐从单线程,到多线程,再到异步编程和并发处理
这些异步与并发的任务不断增加,导致回调结构会变得复杂,为了提高代码的可读性和可维护性,协程(Coroutine)就被引入了
用来简化整个过程的操作
协程是一种特殊的函数,可以在中途暂停,并在稍后恢复执行
与线程不同,协程不会占用独立的系统资源,而是可以由程序直接控制何时暂停和恢复
因此协程其实就是属于某个具体的线程的,可以理解为一个单独的函数,一个单独的执行流,好处就是没有线程的上下文切换的消耗,协程的调度也是自行实现的,会更加灵活
为什么需要协程
简单理解的话,可以理解为,虽然线程已经能够完成绝大部分的任务了,但是线程的粒度还不够精细
举个例子
在网络中,我们调用read函数读取数据,如果缓冲区没有数据的话,整个线程就会一直阻塞到缓冲区有数据,虽然我们可以采用IO多路复用来避免这个问题,但是这也是一种消耗,而且写起来实在不合直觉
如果能把线程细分为更多的子任务就好了,我们可以主动控制多个子任务进行交替执行
假设read函数是一个单独的子任务,当read函数发现缓冲区没有内容时,则直接"阻塞",让出这个线程的执行权,让线程去执行其他的任务
当可读条件满足时,在去唤醒read的子任务,从上次read"阻塞"的地方继续执行
这里的"阻塞"打了引号,因为并非是真的阻塞,而是相当于按下了暂停键,整个线程仍然是没有阻塞的
这样线程的并发就进一步提高了,这里的子任务也可以理解为是一个函数,我们只需要把当前函数的栈空间和寄存器状态保留即可
什么时候使用协程
主要是IO密集型的任务需要,例如网络请求,爬虫,文件读取,web框架等等
对于计算密集型任务还是别用了,因为本身就不需要大量的线程切换,还增加了协程切换的开销
协程的类别
协程在不同的语言实现也不太一样,Python,Java,Go,C++20都支持协程,大致可以分为两大类
- 有栈协程(stackful),也就是类似于内核的线程,协程拥有自己的栈空间,不同协程的切换需要切换对应栈的上下文,只是操作系统不会陷入内核态,例如goroutine,libco
- 无栈协程(stackless),这种就是把上下文会放到公共内存,切换的时候使用状态机来切换,而不用切换上下文,比有栈协程要清凉,C++20,Rust,JavaScript都是这种
这里的有栈和无栈指的是协程之间是否存在调用栈的关系
除此之外还有
- 对称协程,允许协程之间互相调用,可以直接切换到其他的协程,而不必须返回到调用他的协程,可以写你刚才协程环和协程网
- 非对称协程,也叫做一对多的协程,每个协程的控制权只能返回给直接调用他的协程,这种调用关系是单向的
C++20的协程
C++20的协程是属于无栈协程,非对称协程,对协程的实现的底层原理实际上是通过关键字展开,模板来实现的
协程的使用
关键字
主要的关键字有三个
- co_await:暂停协程的执行,等待某个任务的完成
- co_return:返回结果
- co_yield:生成一个中间结果,暂停协程
当一个函数中出现了这三个关键字中的任意一个时,这个函数就会被判定为协程了
我们来写一个简单的协程猜数字,先大概看一下这是个什么东西
可以的话请跟着思路一起写,而非直接复制完整代码来看,这里还是比较复杂的
co_wait
框架
我们先搭一个基本的框架
cpp
#include <coroutine>
#include <iostream>
using namespace std;
struct CoRet
{
};
struct Input
{
};
CoRet Guess()
{
Input input;
int g = co_await input;
cout << "协程:你猜的是 " << g << endl;
}
int main()
{
auto ret = Guess();
cout << "主函数:猜一猜" << endl;
return 0;
}
这里我们定义了两个类,分别是输入和输出,和一个协程Guess,怎么看这是一个协程能,因为其中用来co_await
有些奇怪的点是,Guess没有人恶化的返回语句,但是他可以拥有返回类型,这个返回类型就包含了这个协程的管理句柄,还有其中的一些回调函数
这时候编译器提示我们,他在CoRet这个返回值类型中找不到promise_type这个类
编译器需要我们提供promise_type这个类来对协程进行管理,他会在Guess开始执行是自动初始化promise_type这个类
接下来我们添加上相应的代码
cpp
/// @brief 协程函数的返回值类型
struct CoRet
{
/// @brief 用于控制协程的运行,协程内外数据的交换
struct promise_type
{
};
};
/// 虽然这个函数没有任何的返回语句,但是他仍然可以拥有返回类型
/// 这个返回类型就包含了这个协程的管理句柄,还有一些回调函数
CoRet Guess()
{
// 在这里编译器会隐含的创建一个对象 当然这里是简化过后的
// CoRet::promise_type promise;
Input input;
int g = co_await input;
cout << "协程:你猜的是 " << g << endl;
}
然后这里编译器优惠提示我们, struct promise_type 里面少了一些函数,我们一一介绍
cpp
suspend_never initial_suspend()
{
return {};
}
suspend_never final_suspend() noexcept
{
return {};
}
/// @brief 异常处理
void unhandled_exception()
{
}
首先是这三个 前两个是当协程调用之前和调用结束之后是否需要暂停等待
如何区别呢,就是用suspend_never和suspend_always这两个提供给我们的类,这两个类是可以被直接co_await的对象
suspend_never 不暂停 继续运行
suspend_always 暂停等待 执行逻辑跳回到调用的地方
unhandled_exception当协程出现异常时的处理函数
还少了一个是
cpp
CoRet get_return_object()
{
return
{coroutine_handle<promise_type>::from_promise(*this)};
}
这个函数是用于获取 协程暂停时返回的对象的,这个对象可以是任意一个对象
但是更加通用的对象其实是返回这个协程的管理句柄,不然没有办法管理协程了就
这里用的就是coroutine_handle<promise_type>
类型作为句柄
需要注意的是,这个句柄是需要作为返回值的成员变量的,因此整个CoRet类就是长这样
cpp
/// @brief 协程函数的返回值类型
struct CoRet
{
/// @brief 用于控制协程的运行,协程内外数据的交换
struct promise_type
{
/// @return 提供了两个可以被 co_await 的对象
/// suspend_never 不暂停 继续运行
/// suspend_always 暂停等待 执行逻辑跳回到调用的地方
suspend_never initial_suspend()
{
return {};
}
suspend_never final_suspend() noexcept
{
return {};
}
/// @brief 异常处理
void unhandled_exception()
{
}
/// @note 这个函数是用于获取协程暂停时返回的对象
/// 这里其实可以返回的是一个任意对象
/// 但是没有办法通过协程的返回值来控制协程的进行
/// 最一般的方式是这样的 利用 promise 对象返回一个控制协程运行的句柄
CoRet get_return_object()
{
return
{coroutine_handle<promise_type>::from_promise(*this)};
}
};
// 控制协程运行的句柄
coroutine_handle<promise_type> _h;
};
补全编译器给我们优化的协程就是这样的
cpp
/// @brief
/// @return 协程函数的返回值类型
/// @note
/// 虽然这个函数没有任何的返回语句,但是他仍然可以拥有返回类型
/// 这个返回类型就包含了这个协程的管理句柄,还有一些回调函数
CoRet Guess()
{
// 在这里编译器会隐含的创建一个对象 当然这里是简化过后的
// CoRet::promise_type promise;
// 利用对象产生一个返回的对象,每一次协程被暂停,返回的就是这个对象
// CoRet ret = promise.get_return_object();
// 用创建的对象 co_await initial_suspend 的结果
// co_await promise.initial_suspend();
Input input;
int g = co_await input; // 运行到这里时跳转出去
cout << "协程:你猜的是 " << g << endl;
// co_await promise.final_suspend();
}
然后编译器又提示我们Input类也少东西了
分别是await_ready,await_suspend,await_resume这三个函数
第一个await_ready表示,当遇到co_await 时 是否需要暂停当前协程并跳转回去 如果不需要暂停跳转则返回true 如果需要暂停等待则返回false
cpp
bool await_ready()
{
return false;
}
第二个await_suspend是,在协程即将要被暂停时可以执行的行为
是这样写的
cpp
void await_suspend(coroutine_handle<CoRet::promise_type> h)
{
// h.promise(); 这里协程管理句柄的promise成员函数返回的对象的作用
// 其实就是获取协程的管理对象
}
参数是当前协程的管理句柄 在即将暂停之间,我们可以得到管理句柄,并且执行一些操作
这个函数的返回值指的是 在这个协程在暂停之后,代码执行要跳转到的地方
如果是 void 则跳转到调用这个协程的地方
如果是 其他协程的句柄 则跳转到对应协程继续运行
类似于析构函数和构造函数
调用子类的构造函数时,也必定会自动调用父类的构造函数
这里就是 在遇到暂停之后,可以使用void返回到原来调用协程的地方,也可以继续调用其他协程
不同之处就是这里需要显式写出来
第三个是await_resume, 在 co_wait 需要一个返回值的时候 才需要写
这里的返回值类型是可以自己设定的,可以代表不同的含义
cpp
int await_resume()
{
return 0;
}
一阶段完成
这时候编译器也不再提示什么报错信息了
完整代码是这样
cpp
#include <coroutine>
#include <iostream>
using namespace std;
/// @brief 协程函数的返回值类型
struct CoRet
{
/// @brief 用于控制协程的运行,协程内外数据的交换
struct promise_type
{
/// @return 提供了两个可以被 co_await 的对象
/// suspend_never 不暂停 继续运行
/// suspend_always 暂停等待 执行逻辑跳回到调用的地方
suspend_never initial_suspend()
{
return {};
}
suspend_never final_suspend() noexcept
{
return {};
}
/// @brief 异常处理
void unhandled_exception()
{
}
/// @note 这个函数是用于获取协程暂停时返回的对象
/// 这里其实可以返回的是一个任意对象
/// 但是没有办法通过协程的返回值来控制协程的进行
/// 最一般的方式是这样的 利用 promise 对象返回一个控制协程运行的句柄
CoRet get_return_object()
{
return
{coroutine_handle<promise_type>::from_promise(*this)};
}
};
// 控制协程运行的句柄
// _h.resume() -> 继续运行协程, _h() -> 也是的
coroutine_handle<promise_type> _h;
};
struct Input
{
/// @brief 当遇到 co_await 时 是否需要暂停当前协程并跳转回去
/// @return 不需要暂停跳转返回true | 需要暂停等待返回false
bool await_ready()
{
return false;
}
/// @brief 定义在协程即将要被暂停时的一些行为
/// @param h 当前协程的管理句柄
/// @note 在即将暂停之间,我们可以得到管理句柄,并且执行一些操作
/// @return 这个函数的返回值指的是 在这个协程在暂停之后,代码执行要跳转到的地方
/// 如果是 void 则跳转到调用这个协程的地方
/// 如果是 其他协程的句柄 则跳转到对应协程继续运行
/// 类似于析构函数和构造函数
/// 调用子类的构造函数时,也必定会自动调用父类的构造函数
/// 这里就是 在遇到暂停之后,可以使用void返回到原来调用协程的地方,也可以继续调用其他协程
/// 不同之处就是这里需要显式写出来
void await_suspend(coroutine_handle<CoRet::promise_type> h)
{
// h.promise(); 这里协程管理句柄的promise成员函数返回的对象的作用
// 其实就是获取协程的管理对象
}
/// @brief 在 co_wait 需要一个返回值的时候 才需要写
/// @return 这里的返回值类型是可以自己设定的,可以代表不同的含义
int await_resume()
{
return 0;
}
};
/// @brief
/// @return 协程函数的返回值类型
/// @note
/// 虽然这个函数没有任何的返回语句,但是他仍然可以拥有返回类型
/// 这个返回类型就包含了这个协程的管理句柄,还有一些回调函数
CoRet Guess()
{
// 在这里编译器会隐含的创建一个对象 当然这里是简化过后的
// CoRet::promise_type promise;
// 利用对象产生一个返回的对象,每一次协程被暂停,返回的就是这个对象
// CoRet ret = promise.get_return_object();
// 用创建的对象 co_await initial_suspend 的结果
// co_await promise.initial_suspend();
Input input;
int g = co_await input; // 运行到这里时跳转出去
// co_await 后面可以加的是一个对象 但是也需要加一点东西
cout << "协程:你猜的是 " << g << endl;
// co_await promise.final_suspend();
}
int main()
{
auto ret = Guess(); // 运行到 co_await 时跳转回来
cout << "主函数:猜一猜" << endl;
ret._h.resume(); // 运行到这里时跳转回去
return 0;
}
运行结果是这样的
我们可以画一下协程和主函数的调用关系
数据交换
这里我们可以发现,在CoRet这对象里的promise对象就是沟通协程内外的桥梁
一个自然的想要给协程输入的方法其实就是给这个promise_type对象中新建一个成员变量,然后在Input对象里面存一个promise_type新增的成员变量的地址
在调用await_suspend()时就使用promise对象中的地址对Input对象中的指针赋值,然后再在await_resume()中返回即可
显然这种方法曲折又复杂,没什么人喜欢的
这样我们创建一个新的类 来作为输入,将这个类作为协程的成员变量
这个类在main中初始化,然后通过参数传递给函数,再用协程的Input接收这个参数并初始化协程,就能够完成协程和main函数之间的参数传递了
cpp
struct Note
{
int num;
Note() : num(100) {}
};
struct Input
{
Note ∈
bool await_ready()
{
cout << "await_ready" << endl;
return false;
}
void await_suspend(coroutine_handle<CoRet::promise_type> h)
{
cout << "await_suspend" << endl;
}
int await_resume()
{
cout << "await_resume" << endl;
return in.num;
}
};
CoRet Guess(Note ¬e)
{
Input input{note};
int g = co_await input;
cout << "协程:你猜的是 " << g << endl;
}
int main()
{
Note note;
auto ret = Guess(note);
cout << "主函数:猜一猜" << endl;
ret._h.resume();
return 0;
}
co_yield
这个关键字其实就等价于 co_await promise.yield_value(expr)
这里我们再加一点东西
在Guess开始的时候,产生一个1到114的随机数 int res = (rand() % 114 + 1);
然后从协程外面读取一个数字
再调用co_yield对比较数字的大小
co_yield (res > g ? 1 : (res == g ? 0 : -1));
这里三目运算符的结果无非就是-1 0 1这三种
就相当于调用了这样的函数 co_await promise.yield_value(-1\0\1);
但是我们并没有实现yield_value 因此需要写一下 他的输入其实就是co_yield后面的那个表达式 返回值则是用于区分执行完之后是否需要暂停等待 如果是suspend_never则不等待 如果是suspend_always则进行等待
为了保存这个co_yield输入的参数,我们需要在promise对象中添加一个参数
cpp
suspend_always yield_value(int r)
{
out = r;
return {};
}
int out;
这样当协程比完大小之后,就会把结果赋值到协程管理句柄的out中
然后在主函数中就可以直接取到结果了
cpp
int main()
{
srand(time(nullptr));
Note note;
auto ret = Guess(note); // 运行到 co_await 时跳转回来
cout << "主函数:猜一猜" << endl;
note.num = 10;
ret._h.resume(); // 运行到这里时跳转回去
cout << "主函数:猜的结果是:" << ((ret._h.promise().out == 1) ? "猜大了" : ((ret._h.promise().out == 0) ? "猜对了" : "猜小了"))
<< endl;
return 0;
}
co_return
这个co_return其实和co_yield是差不多的
他也是会调用一个return_value的函数,不同的是,他会在管理句柄中标记这个协程是否已经完成
在协程管理句柄中有一个成员函数是done(),可以获取协程是否执行完成的状态
在这个例子里面,我们想要获取res,同样就是在promise对象中添加一个参数 然后进行赋值就行
cpp
void return_value(int r)
{
res = r;
}
int res;
在Guess的最后调用co_return res;
就会自动转化为co_await return_value(res);
然后在main函数中就可以读取到了
完整代码
cpp
#include <coroutine>
#include <iostream>
using namespace std;
/// @brief 协程函数的返回值类型
struct CoRet
{
/// @brief 用于控制协程的运行,协程内外数据的交换
struct promise_type
{
/// @return 提供了两个可以被 co_await 的对象
/// suspend_never 不暂停 继续运行
/// suspend_always 暂停等待 执行逻辑跳回到调用的地方
suspend_never initial_suspend()
{
cout << "initial_suspend" << endl;
return {};
}
suspend_never final_suspend() noexcept
{
cout << "final_suspend" << endl;
return {};
}
/// @brief 异常处理
void unhandled_exception()
{
}
/// @note 这个函数是用于获取协程暂停时返回的对象
/// 这里其实可以返回的是一个任意对象
/// 但是没有办法通过协程的返回值来控制协程的进行
/// 最一般的方式是这样的 利用 promise 对象返回一个控制协程运行的句柄
CoRet get_return_object()
{
cout << "get_return_object" << endl;
return {coroutine_handle<promise_type>::from_promise(*this)};
}
suspend_always yield_value(int r)
{
out = r;
return {};
}
int out;
void return_value(int r)
{
res = r;
}
int res;
};
// 控制协程运行的句柄
// _h.resume() -> 继续运行协程, _h() -> 也是的
coroutine_handle<promise_type> _h;
};
struct Note
{
int num;
};
struct Input
{
Note ∈
/// @brief 当遇到 co_await 时 是否需要暂停当前协程并跳转回去
/// @return 不需要暂停跳转返回true | 需要暂停等待返回false
bool await_ready()
{
cout << "await_ready" << endl;
return false;
}
/// @brief 定义在协程即将要被暂停时的一些行为
/// @param h 当前协程的管理句柄
/// @note 在即将暂停之间,我们可以得到管理句柄,并且执行一些操作
/// @return 这个函数的返回值指的是 在这个协程在暂停之后,代码执行要跳转到的地方
/// 如果是 void 则跳转到调用这个协程的地方
/// 如果是 其他协程的句柄 则跳转到对应协程继续运行
/// 类似于析构函数和构造函数
/// 调用子类的构造函数时,也必定会自动调用父类的构造函数
/// 这里就是 在遇到暂停之后,可以使用void返回到原来调用协程的地方,也可以继续调用其他协程
/// 不同之处就是这里需要显式写出来
void await_suspend(coroutine_handle<CoRet::promise_type> h)
{
cout << "await_suspend" << endl;
// h.promise(); 这里协程管理句柄的promise成员函数返回的对象的作用
// 其实就是获取协程的管理对象
}
/// @brief 在 co_wait 需要一个返回值的时候 才需要写
/// @return 这里的返回值类型是可以自己设定的,可以代表不同的含义
int await_resume()
{
cout << "await_resume" << endl;
return in.num;
}
};
/// @brief
/// @return 协程函数的返回值类型
/// @note
/// 虽然这个函数没有任何的返回语句,但是他仍然可以拥有返回类型
/// 这个返回类型就包含了这个协程的管理句柄,还有一些回调函数
CoRet Guess(Note ¬e)
{
// 在这里编译器会隐含的创建一个对象 当然这里是简化过后的
// CoRet::promise_type promise;
// 利用对象产生一个返回的对象,每一次协程被暂停,返回的就是这个对象
// CoRet ret = promise.get_return_object();
// 用创建的对象 co_await initial_suspend 的结果
// co_await promise.initial_suspend();
int res = (rand() % 114) + 1;
Input input{note};
int g = co_await input; // 运行到这里时跳转出去
// co_await 后面可以加的是一个对象 但是也需要加一点东西
cout << "协程:你猜的是 " << g << endl;
co_yield (res > g ? 1 : (res == g ? 0 : -1));
co_return res;
// co_await promise.final_suspend();
}
int main()
{
srand(time(nullptr));
Note note;
auto ret = Guess(note); // 运行到 co_await 时跳转回来
cout << "主函数:猜一猜" << endl;
note.num = 10;
ret._h.resume(); // 运行到这里时跳转回去
cout << "主函数:猜的结果是:" << ((ret._h.promise().out == 1) ? "猜大了" : ((ret._h.promise().out == 0) ? "猜对了" : "猜小了"))
<< endl;
cout << "主函数:随机数是:" << ret._h.promise().res << endl;
return 0;
}