什么是协程
我们在学习编程的过程中,逐渐从单线程,到多线程,再到异步编程和并发处理
这些异步与并发的任务不断增加,导致回调结构会变得复杂,为了提高代码的可读性和可维护性,协程(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
框架
我们先搭一个基本的框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #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这个类
接下来我们添加上相应的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| struct CoRet { struct promise_type { }; };
CoRet Guess() { Input input; int g = co_await input; cout << "协程:你猜的是 " << g << endl; }
|
然后这里编译器优惠提示我们, struct promise_type 里面少了一些函数,我们一一介绍
1 2 3 4 5 6 7 8 9 10 11 12 13
| suspend_never initial_suspend() { return {}; } suspend_never final_suspend() noexcept { return {}; }
void unhandled_exception() {
}
|
首先是这三个 前两个是当协程调用之前和调用结束之后是否需要暂停等待
如何区别呢,就是用suspend_never和suspend_always这两个提供给我们的类,这两个类是可以被直接co_await的对象
suspend_never 不暂停 继续运行
suspend_always 暂停等待 执行逻辑跳回到调用的地方
unhandled_exception当协程出现异常时的处理函数
还少了一个是
1 2 3 4 5
| CoRet get_return_object() { return {coroutine_handle<promise_type>::from_promise(*this)}; }
|
这个函数是用于获取 协程暂停时返回的对象的,这个对象可以是任意一个对象
但是更加通用的对象其实是返回这个协程的管理句柄,不然没有办法管理协程了就
这里用的就是coroutine_handle<promise_type>
类型作为句柄
需要注意的是,这个句柄是需要作为返回值的成员变量的,因此整个CoRet类就是长这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| struct CoRet { struct promise_type { suspend_never initial_suspend() { return {}; } suspend_never final_suspend() noexcept { return {}; } void unhandled_exception() {
} CoRet get_return_object() { return {coroutine_handle<promise_type>::from_promise(*this)}; }
}; coroutine_handle<promise_type> _h; };
|
补全编译器给我们优化的协程就是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
CoRet Guess() {
Input input; int g = co_await input; cout << "协程:你猜的是 " << g << endl;
}
|
然后编译器又提示我们Input类也少东西了
分别是await_ready,await_suspend,await_resume这三个函数
第一个await_ready表示,当遇到co_await 时 是否需要暂停当前协程并跳转回去 如果不需要暂停跳转则返回true 如果需要暂停等待则返回false
1 2 3 4
| bool await_ready() { return false; }
|
第二个await_suspend是,在协程即将要被暂停时可以执行的行为
是这样写的
1 2 3 4 5 6
| void await_suspend(coroutine_handle<CoRet::promise_type> h) { }
|
参数是当前协程的管理句柄 在即将暂停之间,我们可以得到管理句柄,并且执行一些操作
这个函数的返回值指的是 在这个协程在暂停之后,代码执行要跳转到的地方
如果是 void 则跳转到调用这个协程的地方
如果是 其他协程的句柄 则跳转到对应协程继续运行
类似于析构函数和构造函数
调用子类的构造函数时,也必定会自动调用父类的构造函数
这里就是 在遇到暂停之后,可以使用void返回到原来调用协程的地方,也可以继续调用其他协程
不同之处就是这里需要显式写出来
第三个是await_resume, 在 co_wait 需要一个返回值的时候 才需要写
这里的返回值类型是可以自己设定的,可以代表不同的含义
1 2 3 4
| int await_resume() { return 0; }
|
一阶段完成
这时候编译器也不再提示什么报错信息了
完整代码是这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
| #include <coroutine> #include <iostream> using namespace std;
struct CoRet { struct promise_type { suspend_never initial_suspend() { return {}; } suspend_never final_suspend() noexcept { return {}; } void unhandled_exception() {
} CoRet get_return_object() { return {coroutine_handle<promise_type>::from_promise(*this)}; }
}; coroutine_handle<promise_type> _h; };
struct Input { bool await_ready() { return false; }
void await_suspend(coroutine_handle<CoRet::promise_type> h) { }
int await_resume() { return 0; } };
CoRet Guess() {
Input input; int g = co_await input; cout << "协程:你猜的是 " << g << endl;
}
int main() { auto ret = Guess(); cout << "主函数:猜一猜" << endl; ret._h.resume(); return 0; }
|

运行结果是这样的
我们可以画一下协程和主函数的调用关系

数据交换
这里我们可以发现,在CoRet这对象里的promise对象就是沟通协程内外的桥梁
一个自然的想要给协程输入的方法其实就是给这个promise_type对象中新建一个成员变量,然后在Input对象里面存一个promise_type新增的成员变量的地址
在调用await_suspend()时就使用promise对象中的地址对Input对象中的指针赋值,然后再在await_resume()中返回即可
显然这种方法曲折又复杂,没什么人喜欢的
这样我们创建一个新的类 来作为输入,将这个类作为协程的成员变量
这个类在main中初始化,然后通过参数传递给函数,再用协程的Input接收这个参数并初始化协程,就能够完成协程和main函数之间的参数传递了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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对象中添加一个参数
1 2 3 4 5 6
| suspend_always yield_value(int r) { out = r; return {}; } int out;
|
这样当协程比完大小之后,就会把结果赋值到协程管理句柄的out中
然后在主函数中就可以直接取到结果了
1 2 3 4 5 6 7 8 9 10 11 12
| int main() { srand(time(nullptr)); Note note; auto ret = Guess(note); 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对象中添加一个参数 然后进行赋值就行
1 2 3 4 5
| void return_value(int r) { res = r; } int res;
|
在Guess的最后调用co_return res;
就会自动转化为co_await return_value(res);
然后在main函数中就可以读取到了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
| #include <coroutine> #include <iostream> using namespace std;
struct CoRet { struct promise_type { suspend_never initial_suspend() { cout << "initial_suspend" << endl;
return {}; } suspend_never final_suspend() noexcept { cout << "final_suspend" << endl;
return {}; } void unhandled_exception() { } 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; }; coroutine_handle<promise_type> _h; };
struct Note { int num; };
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) { int res = (rand() % 114) + 1; Input input{note}; int g = co_await input; cout << "协程:你猜的是 " << g << endl; co_yield (res > g ? 1 : (res == g ? 0 : -1)); co_return res; }
int main() { srand(time(nullptr)); Note note; auto ret = Guess(note); 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; }
|