Demystifying C++20 Coroutines
Introduction
自 C++11 开始,很多更新都集中在并发支持上,从最初的线程基础支持,到如今的协程,C++ 已经日趋完善。
协程是继线程之后的又一利器,若论年龄,协程倒比线程要大。由于早期的线程主要在单 CPU 上运行,仅是模拟多线程,故又称“伪线程”。伪线程和协程的思路颇有相似之处,以致难以旌别,其实它们各有应用场景。
本篇将回答下列问题:
- 什么是协程?
- 协程与进程和线程有何区别?
- 什么是并发,和并行有何区别?
- 协程和函数有何区别?
- 什么是有栈协程,无栈协程?
- 协程的使用场景有哪些?
- Coroutines TS 是什么?
- 如何使用 C++20 Coroutines?
本篇旨在明析概念,不会出现太多代码,概念清晰后,下篇将有大量代码来解析协程。
Concurrency versus Parallelism
要理清协程的定位,须得先能够旌别并发与并行。可以先来看看百科的定义:
并行是指“并排行走”或“同时实行或实施”。
在操作系统中是指,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。
对比地,并发是指:在同一个时间段内,两个或多个程序执行,有时间上的重叠(宏观上是同时,微观上仍是顺序执行)。
这里用了两个词:宏观和微观。宏观指从大的角度来看,微观指从小的角度来说。通俗地说,宏观指的是能够通过眼睛看到的,微观则是眼睛看不到的。并发在宏观上同时,微观上为顺序执行。这就是伪线程的实现思路,通过迅速地在各个模块之间切换执行,以迷惑视觉,使得看起来像是同时执行的,其实底层依旧是顺序执行的。
线程在早期并非必须,因为都是命令窗口,就是从上往下依次执行的。而 Windows 拥有界面,在执行任务之时可以随意进行别的操作,因此界面常常会卡死。MS 便搞了个模拟同步运行的机制,便是线程。了解过 CPU 发展历史的会知道,早期 CPU 频率步步直升,人们都以为这种趋势能够持续下去。这也意味着你写的程序不用更新也能随着 CPU 的增强而自动增强性能。
所以早期都是单核 CPU。随着物理瓶颈到来,频率提升之路步步维艰,生产厂商便转变策略,通过将多个核心置于一起,产生多核CPU来提升性能。这便是性能不够,数量来凑。也因此,真正的多线程得以实现。
真线程的执行便为并行执行。由于每个核心都是独立的,因此可以同时执行。此时,无论是视觉上,还是底层实现上,都是真正的同时执行。
既然有并发与并行两种形式,那么我们对其进行排列组合,便能得出4种结果。
一个既无并行,也无并发的程序处于并发的能力最弱,只能顺序执行每条任务,这一般是一个很小的程序。当有并发,无并行时,能够充分发挥单核CPU的性能,伪线程与协程便属此列。而无并发,有并行时,也就是说是多核CPU,但一个核心上只开了一个线程,此时的确是并发。但和有并发无并行时所达到的效果是一样的,真线程便属此列。
也正因如此,线程和协程的效果只用眼睛是无法分辨的。有很多人便说有了多线程为啥还要加入协程呢?其实协程和线程分工并不同,一个是并发,一个是并行,不过伪线程也属于并发罢了。
那现在你可能又要改变问题了,既然伪线程也是并行,那么请问协程和伪线程又如何区分?
这个问题才是关键,其实主要区别是调度问题,本文第四节将会详细对比协程与线程。
只有并发,或只有并行,都无法处理高并发需求,所以二者呈互补之势,共同发挥CPU的最大性能。
下面再以一个例子来谈谈并发与并行。
假设有一个迷宫。
现欲寻得迷宫出口,便有四种方式。
在无并发无并行的情况下,只能暴力式的遍历每条路径。因此,在发现一条路不通时,需要原路返回到分岔路口,以接着尝试另一条路径。这意味着很多条路径都要走两次,效率可想而知。
在无并发有并行的情况下,便意味着有多人在同时寻找出口,即使其中一个人走的是死角,其他人亦可继续自己的任务。如此一来,每条路便只需走一遍。当然,这也意味着所耗费的资源也更多,此处对应的便是人力,在计算机中,对应的便是 CPU 核心数。核心数无法持续增加,所以此处便有些是理想状态了,实际情况可能得一人负责多条路径。也就是说一个核上分成多个线程,此时便是真伪线程混合,并发与并行混合了。
那么只有并发是什么情况呢?这时的情况是这样的:一个人先走一部分,接着其他人再走,因为是并行执行,所以这些人无法同时行走。其中一个人走一会儿,停下来,其他人才能接着走。
这样的好处是什么呢?
即使一个人走入死胡同,也无需再原路返回。简单地说,当每个人停止行走时,都需要转到其他人那里去执行。他怎么瞬间跑到其他人那里去呢?其实每个人在跳转之前,都会在原地插一个眼做标记,当需要跳转时,便可直接传送过去。所以在一个人步入死角时,便会放弃这条路,传送到其他人那里继续执行。最后留下的一个人,便是正确的迷宫出口。和并行不同,并发所需的资源要少,可以开启上亿个协程行动,他们之间互相协作,无论多大的迷宫,也能很快找到出口。
最后,并行与并发结合,无疑会指数级提升性能,并发说到底同时还是只有一个人能行动,而并发能同时多人行动,两者互补,才能发挥出最大效率。
Quantum computation
上述所说的统一称为经典计算,既然谈到了并行,便不得不再说下量子计算。
通过上面介绍,可知经典计算要增加计算能力,先是尝试提高处理器频率,无奈达到了物理瓶颈。于是便增加物理资源,用数量来凑。然而,这样的方法来增加计算能力同样是不可持续的,而且增加核心要消耗更多的物理资源。
第一个分岔路口需要2个人,第二个便要4个人,接着便要8个,16个,32个…… 这个数字是呈指数增长的,要不了多久,便会成为天文数字。这也是现如今许多密码学加密技术的依赖,通常数字都会非常大,计算机跑几十年可能都跑不出来。这就是经典计算的瓶颈,理论上算力不可达。
经典计算的算力只能线性增加,而问题规模却是指数增长的,所以经典计算根本追不上问题增长的速度。而量子计算则不同,随着量子比特的增大,其信息容量也会呈指数增长。
像上面的迷宫,若是由量子计算来遍历会如何做呢?
比如其有 10 个人,那么便能同时分出 1024 个分身,这样所有的路径都能被搜索到,只需走一次,就能找出迷宫出口。这被称为量子叠加态的并行演化。同时,量子还有另一个特性——「纠缠性」,可以解决在开发并行程序时需要格外观注的数据竞争问题,通常都是通过锁或原子量来同步数据。
量子纠缠特性使量子计算天生就带有同步性,一个数据被改变了,不论别处还有多少个数据,瞬间全部都会改变。比如有一个分身死了,那么1024个分身便全死了。
所以面对暴力计算,量子计算都只表示笑笑不说话。量子计算目前还在继续发展,当下,我们只能使用经典计算来写并发程序。
Coroutines
注:本节介绍的协程不具体指某一语言中的,示例亦为伪码,C++20 的协程将于第五节介绍。
弄清楚了并发概念,便可正式开始讲协程了,先来看看 wiki 上的定义:
Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed. Coroutines are well-suited for implementing familiar program components such as cooperative tasks, exceptions, event loops, iterators, infinite lists and pipes.
简言之,协程,用于实现协作式多任务,属于同一个进程的多个协程,在同一时刻只有一个处于运行状态。
协程属于并发形式的一种创建方式,它由两部分组成的:Functions 和 Cooperative。可以说,Functions 之间拥有了 Cooperative 能力,便称之为 Coroutines。
比如,现有一个普通的函数:
auto happy()
{
return ": )"; // 在此处传值并退出函数
}
auto emoji = happy(); // 在此处调用函数
此函数返回一个字符串,可以直接进行输出。当调用函数时,只能等到函数执行结束才算结束调用。
现在对表情进行扩展。
void emoji()
{
vector<string> emoji { ": )", ": (", "^-^", "@_@", "=^=" };
for(auto p = emoji.begin(); p != emoji.end(); ++p)
{
cout << *p << " ";
}
cout << endl;
}
现在,我们想在不改变该函数的情况下进入如下输出:
1. : )
2. : (
3. ^-^
4. @_@
5. =^=
于是需要引出另一个函数:
void add_lines()
{
for(int i = 1;; ++i)
{
cout << i << ": ";
cout << endl;
}
}
可是,即使如此,依旧无法得到想要的输出。此时,只有这两个函数互相协作才能完成任务。
注意这两个函数的执行顺序,他们是交叉执行的,而非像普通函数那样调用后执行完直接返回。
这里有几个关键点。
第一,被调用函数(emoji)需要知道调用函数(add_lines)的返回地址,这样才能在输出后再跳回到调用方继续执行。
第二,需要记住先前局部变量的值,用于在下一次执行时恢复现场。
第三,被调用方可以主动让出控制流,以让调用方恢复执行。
第四,调用方可以主动恢复被调用方的执行,此时会从上次暂停的地方继续执行。
实际上,协程本质上就是对控制流的主动让出和恢复机制,这也是其与函数之间的区别。
协程是函数的增强版,函数是协程的弱化版。
函数只能启动(start)和终止(finish)执行,而协程在此基础上,增加了挂起(suspend)和恢复(resume)能力。
那么如何完成上述流程呢?
可以来增加挂起和恢复机制:
// !!!伪码表示,仅作说明之用
struct coro_frame {
using iter_type = vector<string>::iterator;
iter_type iter;
int index;
resume() {
switch(index) {
case 0:
goto flag_r0; // 从flag_r0恢复执行
default:
goto flag_default;
}
}
};
coro_frame* emoji()
{
void* handle = CORO_BEGIN(malloc); // 分配状态所需的空间
coro_frame* frame = (coro_frame*)handle; // 转换为frame
vector<string> emoji { ": )", ": (", "^-^", "@_@", "=^=" };
for(frame->iter = emoji.begin(); frame->iter != emoji.end(); ++frame->iter)
{
frame.index = 0; // 记录暂停点
CORO_SUSPEND(handle); // 暂停执行,挂起协程
RETURN_OBJECT(frame); // 返回控制句柄,以便从外部恢复
flag_r0:
print(frame->iter);
}
println();
CORO_END(hdl, free); // 协程结束,销毁申请的空间
}
void add_lines()
{
auto frame = await emoji();
for(int i = 1;; ++i)
{
println(i);
frame.resume(); // 在此恢复被调用方
println();
}
}
可以看到,我们对原有代码添加了许多额外代码来满足挂起恢复的能力,实际上编译器在生成协程时也会有这样的操作,不过那是一个非常完善的机制了。
相信通过上述内容,相信大家已经对协程有了大致的印象,那么本节的任务也就完成了。
Stackful Coroutines versus Stackless Coroutines
函数在调用之前会将所需的参数压入栈中以供使用,局部的数据也都保留在栈中,这一系列所需的数据称为状态(state),需要在调用方和被调用方之间来保存这些状态,才能完成函数的调用和返回动作。
协程也需要保存一些状态,而且因为要支持挂起和恢复功能,所以要保存的状态也要比函数多。协程需要保存如下状态:
- Promise 对象
- 各个形参
- 当前挂起点的位置,以便恢复时跳转
- 局部变量和临时量
此时,便会有不同方式来保存这些状态。主流的两种,一种是有栈协程,另一种是无栈协程。
有栈协程,顾名思义,将数据保存在栈中;而无栈协程,会将主要数据采用堆来动态分配,只有少量状态存放在栈上,此时只能挂起处于停层的函数。
有栈协程的生命期和它们的栈一样长,无栈协程的生命期和它们的对象一样长;有栈协程的数据在被调用方分配,而无栈协程的数据在调用方分配。
关于此二者的区别,已经有文章详细描述,可以参考这篇文章:Coroutines Intruduction。
其实现在了解与否并无关紧要,只需记往 C++20 的协程是无栈协程便可。
Distinguish between process, thread and coroutine
关于进程线程,过去已经写过一些文章了,此处便不再详细介绍,关键是需要弄清楚并发和并行的区别,这也在前文进行了详细介绍。
现在,只需下表的对比,就能清晰地了解这三者之间的关系与差别。
进程 | 线程 | 协程 | |
---|---|---|---|
切换者 | 操作系统 | 操作系统 | 用户(编程者/应用程序) |
切换时机 | 根据操作系统自己的切换策略,用户不感知 | 根据操作系统自己的切换策略,用户不感知 | 用户自己(的程序)决定 |
切换内容 | 页全局目录 内存栈 硬件上下文 |
内核栈 硬件上下文 |
硬件上下文 |
切换内容的保存 | 保存于内核栈中 | 保存于内核栈中 | 保存于用户自己的变量(用户栈或堆) |
切换过程 | 用户态-内核态-用户态 | 用户态-内核态-用户态 | 用户态(未陷入内核态) |
切换效率 | 低 | 中 | 高 |
正是因为进程和线程的上下文切换效率低,所以才加入了协程来提高并发的能力。
C++20 Coroutines TS
First Look at C++ Coroutines
如前文所述,协程是一个分步执行,遇到条件会挂起,直到满足条件才会被唤醒以继续执行后面代码的并行方式。
C++ 准备了许久,终于在今年加入了协程,不过 C++20 的协程标准仍处于「原理层」,只包含了编译器需要实现的底层功能,是专门给库开发者使用的,并没有提供属于「应用层」的高级库给普通程序员使用。
这意味着要想使用协程,要么自己从原理层学起,自己封装所需的功能,要么等待 C++23 的标准协程库或第三方库。
我们自然选择前者,因此要学习的东西就比较多了,本篇就是先带大家入协程的门,后面再层层深入。
C++20 提供了三个新的关键字来支持协程:
co_await
co_yield
co_return
只要一个函数中包含了上面三个关键字中的任意一个,那么这个函数就是一个协程。co_await
可以挂起和恢复函数的执行,是主要需要学习的一个关键字;co_yield
可以在不结束协程的情况下从协程返回一些值。因此,可以用它来编写无终止条件的生成器函数;co_return
允许从协程返回一些值,不过这需要稍加定制。
比如一个生成器函数:
generator<int> generate_numbers(int begin, int inc = 1)
{
for(int i = begin;; i += inc)
co_yield i;
}
因为 generate_numbers()
中包含了 co_yield
关键字,所以它就是一个协程。你也许已经看到,该协程没有结束条件,因此它可以无限地产生值。
但就此一点代码可还无法完成工作,稍后再来完善它,在此之前,还需了解几个重要的组件。
Custom Coroutine Functor and Custom Promise
我们已经知道可通过 co_yield
来挂起协程,但是挂起之后又该如何恢复呢?
因此,需要有一种方式可以和协程进行沟通,当协程挂起的时候,通过这种方式来进行恢复。
这种方式便是 Custom Coroutine Functor(定制协程函数/仿函数),前面所写生成器的返回类型 generator
便是我们对于生成器所定义的 Coroutine Functor。
注:前面使用了协程关键字的 generate_numbers 也叫协程函数,为避免和此处的 Coroutine Functor 翻译混淆,本文中所有协程函数都指前者,Coroutine Functor 均以英文出现。
现在,来定义它:
template <typename T> class generator
{
public:
// 恢复协程
bool resume();
};
在该 Coroutine Functor 中,声明了用于恢复协程的 resume
函数。
现在,进行编译程序,可以获取如下错误:
编译器告诉我们,promise_type
不是 generator
的成员,因此,在 Coroutine Functor 中需要遵循一些规范。
我们需要在成员中加入 promise_type
,这个东西称为 Promise 对象,需要定义以下接口:
get_return_object
initial_suspend
final_suspend
return_void
un_handled_exception
当使用协程函数时:
auto nums = generate_numbers(1);
首先会挂起当前执行点,此时便会调用 initial_suspend
。接着,通过 get_return_object
将 Coroutine Functor 返回给调用方,因此在稍后才能进行恢复。当遇到未处理的异常时,便会调用 un_handled_exception
,可以在此进行捕获。结束时,会调用用 return_void
,来到 final_suspend
,协程结束,Coroutine Functor 析构。
由这些接口可知,协程通过 Promise 对象来提交结果或返回异常。
现在来实现这些接口:
template <typename T> class generator
{
public:
struct promise_type {
using coro_handle = std::experimental::coroutine_handle<promise_type>;
auto get_return_object() {
return coro_handle::from_promise(*this);
}
auto initial_suspend() { return std::experimental::suspend_always{}; }
auto final_suspend() { return std::experimental::suspend_always{}; }
auto return_void() {}
void un_handled_exception() { std::terminate(); }
};
bool resume();
};
接口俱以实现。可以发现其中又新增了两个东西:
coroutine_handle
suspend_always
下一步之前,需要先了解这些概念。
Coroutine handle
Promise 对象从协程内部操纵,用于提交结果和异常,而 Coroutine handle(协程句柄)则从协程外部操纵,专门用于管理协程上下文,可以恢复或销毁协程。
与 Promise 对象不同,Coroutine handle 在标准文件已经定义:
template <>
struct coroutine_handle<void>; // no promise access
template <typename _PromiseT>
struct coroutine_handle : coroutine_handle<>; // general form
可以看到,有两个形式,一个 void
特化版和一个通用形式。
Coroutine handle 主要包含了以下几个重要成员:
static coroutine_handle from_address(void*)
void* address()
void operator()()
explicit operator bool()
void resume()
void destroy()
bool done()
Coroutine Functor 的恢复操作,其实就是通过 Coroutine handle 的 resume
函数进行恢复协程的,拥有了它,也就拥有了控制协程的能力。
既然 Coroutine handle 这么重要,那么如何为我们的 Coroutine Functor 配备上呢?
首先,编译器如何把 Coroutine handle 给我们?想要就得主动点,只需在 promise_type
中自己先定义一个 co_handle
类型:
struct promise_type {
using coro_handle = std::experimental::coroutine_handle<promise_type>;
auto get_return_object() {
return coro_handle::from_promise(*this);
}
...
由此,便给 promise_type
配上了 Coroutine handle,通过 from_promise
函数,可以从 promise 来创建 Coroutine handle。也可直接使用 return generator{};
。之后,调用 get_return_object
函数便能得到匹配的 Coroutine handle。
前面说过,Promise 对象是从协程内部操纵的,因此 get_return_object
函数其实是给编译器调用的。
那么搞了半天,我们要怎么用呢?
当协程首次挂起时,编译器便会使用 operator new
分配空间,保存当前作用域内的各种信息,以在恢复时使用。随后便会调用 get_return_object()
,并将此结果返回给调用方,也就是 Coroutine Functor。
因此,我们需要在那时进行保存。
template <typename T> class generator
{
public:
struct promise_type {
...
};
using coro_handle = std::experimental::coroutine_handle<promise_type>;
generator(coro_handle handle) : handle_(handle) { assert(handle_); }
generator(const generator&) = delete;
generator(generator&& other) : handle_(other.handle_) { other.handle_ = nullptr; };
~generator() { handle_.destroy(); }
bool resume() {
if (!handle_.done())
handle_.resume();
return !handle_.done();
}
private:
coro_handle handle_;
};
可以看到,编译器实际上调用了我们的构造函数,因此,必须提供 Coroutine handle 类型的构造函数。
之后,便可对协程进行操控。可以看到,当前已经可以使用 resume
函数来恢复协程了。
Awaitable object
Awaitable object 叫作可等待的对象,或者应该说是拥有等待属性的对象。
何时挂起,何时恢复,一定需要根据一个条件来决定。比如,当服务器接收数据时,在等待数据的时候便挂起,去执行别的逻辑。待数据到来,便恢复接收数据的操作。
这个对于条件的抽象便是 Awaitable object,需要满足三个接口:
bool await_ready()
void await_suspend(coroutine_handle<>)
auto await_resume()
每次调用 co_await
时,便会调用 await_ready()
来查看是否已满足条件,满足表示万事俱备,编译器会调用 await_resume()
恢复协程;若条件尚未准备完成,则会调用 await_suspend()
挂起协程,将控制权返还给调用者。
标准中提供了两个 trival Awaitable object:
suspend_never
{
bool await_ready() { return true; }
void await_suspend(coroutine_handle<>) {}
auto await_resume() {}
};
suspend_always
{
bool await_ready() { return false; }
void await_suspend(coroutine_handle<>) {}
auto await_resume() {}
};
suspend_always
的 await_ready()
总是返回 false
,suspend_never
的 await_ready()
总是返回 true
。通常情况这两个已经可以满足需求了,但也有很多情况需要自己编写特定版本,这些放到下篇专门来介绍。
Coroutine traits
回到生成器的协程函数:
generator<int> generate_numbers(int begin, int inc = 1)
{
for(int i = begin;; i += inc)
co_yield i;
}
编译器需要从返回类型 generator<int>
来确定 Promise 的类型,其所使用的萃取特性的工具便是 Coroutine traits(协程特性),标准中自带了一个 std::coroutine_traits
,便是其实现。
所以我们的协程函数的 Promise 类型将被推导为:
std::coroutine_traits<generator<int>, int, int>::promise_type
co_yield
, co_await
, and co_return
到此为止,大家基本已经了解这三个关键字的概念。
接下来将继续完成 co_yield
所完成的生成器并介绍 co_return
,co_await
是协程的关键角色,放到下篇专门来论。
因为要使用 co_yield
,所以还需给 Promise 增加一个 yield_value
接口:
template <typename T> class generator
{
public:
struct promise_type {
...
T value_;
auto yield_value(T value) {
value_ = value;
return std::experimental::suspend_always{};
}
};
};
yield_value
将数据保存到 Promise 中,取出操作须自己定义:
template <typename T> class generator
{
public:
struct promise_type {
...
T value_;
// 保存生成的值
auto yield_value(T value) {
value_ = value;
return std::experimental::suspend_always{};
}
};
...
// 获取生成的值
const T get_generated_value() {
return handle_.promise().value_;
}
private:
coro_handle handle_;
};
与 yield_value
不同的是,get_generated_value
是自定义的,名称随意。
现在,便可以使用我们的协程了:
int main()
{
auto nums = generate_numbers(1);
for (;;)
{
nums.resume();
std::cout << nums.get_generated_value() << " ";
if (nums.get_generated_value() > 20) break;
}
}
/*
* Output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
*/
此处,使用生成器从 1
开始,依次以递增 1
的趋势生成数字,现在的结束条件完全在调用方这边。因此,所有流程,都可由调用方进行控制。
最后,简单地介绍下 co_return
,它用于协程的返回,就像 return
一样,不过是专门针对协程的。
例如:
std::future<int> a() {
co_return 42;
}
当协程没有结束条件时,就像上述定义的生成器,则需要定义 return_void()
或 return_value()
,若无定义,当协程结束时会遇到未定义的行为(一般没定义会直接编译不过)。
如果通过 co_return
返回 void
,则会调用 return_void()
; 通过 co_return
返回一些值时,则会调用 return_value()
。这两个函数不能同时定义。
本节到此便已结束,相信大家对 C++ 的协程也有了初步的认识,后面便能再写一些稍微有难度的文章来继续分享。
本节完整的协程代码如下:
#include <iostream>
#include <cassert>
#include <experimental/coroutine>
template <typename T> class generator
{
public:
struct promise_type {
using coro_handle = std::experimental::coroutine_handle<promise_type>;
auto get_return_object() {
return coro_handle::from_promise(*this);
}
auto initial_suspend() { return std::experimental::suspend_always{}; }
auto final_suspend() { return std::experimental::suspend_always{}; }
auto return_void() {}
void un_handled_exception() { std::terminate(); }
T value_;
auto yield_value(T value) {
value_ = value;
return std::experimental::suspend_always{};
}
};
using coro_handle = std::experimental::coroutine_handle<promise_type>;
generator(coro_handle handle) : handle_(handle) { assert(handle_); }
generator(const generator&) = delete;
generator(generator&& other) : handle_(other.handle_) { other.handle_ = nullptr; };
~generator() { handle_.destroy(); }
bool resume() {
if (!handle_.done())
handle_.resume();
return !handle_.done();
}
const T get_generated_value() {
return handle_.promise().value_;
}
private:
coro_handle handle_;
};
generator<int> generate_numbers(int begin, int inc = 1)
{
for (int i = begin;; i += inc)
co_yield i;
}
int main()
{
auto nums = generate_numbers(1);
for (;;)
{
nums.resume();
std::cout << nums.get_generated_value() << " ";
if (nums.get_generated_value() > 20) break;
}
return 0;
}
Coroutines use cases
- generators
- 异步IO
- lazy computations
- 事件驱动程序
- 协作式多任务
- 无限列表
- ……
Conclusion
本篇介绍了 C++20 协程的大多数概念,理解之后便算是协程入门了。
关于文章开头的那些问题,想必大家心中也已有了答案。
但这些介绍仍只是其冰山一角,C++ 的协程还有非常多的知识需要学习,待日后再慢慢道来。
References
- https://en.cppreference.com/w/cpp/language/coroutines
- https://en.m.wikipedia.org/wiki/Coroutine
- https://baike.baidu.com/item/%E5%B9%B6%E8%A1%8C/5806759#viewPageContent
- https://blog.panicsoftware.com/coroutines-introduction/
- https://blog.panicsoftware.com/your-first-coroutine/
- http://www.davespace.co.uk/arm/introduction-to-arm/registers.html
- https://owent.net/2019/1904.html
- https://blog.csdn.net/ericzhangali/article/details/1330648
- https://www.techopedia.com/definition/8583/non-preemptive-multitasking
- Concurrency with modern c++
- C++20 Coroutines – Milosz Warzecha
- CppCon2014
- C++ Coroutines CppCon2015
- C++ Coroutines-under the covers CppCon2016
- Coroutines in C++17
- N4024 Distinguishing coroutines and fibers
- N4680 C++ Extensions for Coroutines
- N4760 Working draft, c++ extensions for coroutines