一、前言
自从换工作以后,已经少有业务学习技术的时间了,对于大的知识点的积累和更新变得比较困难。而本文的知识点——了解协程 适合作为一个在紧凑的工作生活中学习的技术。协程本身不难,但是要了解其中精髓并写出好用的协程应用是需要花费一定时间的。本文将初步的讲解协程的基础知识,并结合一段开源代码进行讲解,最后会通过修改代码完成一定的功能修改,尽量使读者可以基本了解协程
本文前置知识:
进程
线程
上下文切换
二、正文
2.1 传统调用
在了解 协程 之前,我们需要了解一下传统执行流中是如何完成我们的任务的。
通常来说,函数 或者 称为 子程序、子例程 都是层级调用的,比如 下面的伪代码中:
int A()
{
a()
}
int func()
{
A()
B()
C()
}
如果上述代码使用 线程
进行调用函数 func()
,那么就会是:
- 调用
A()
,进入A()
执行a()
,然后返回 - 调用
B()
后返回 - 调用
C()
返回
上述函数如果在执行过程中被系统切换出去,回来继续从原来的地方继续执行。其 调用顺序是明确的 。
2.2 协程调用
结合 进程
和 线程
,其各自的基本概念如下:
-
进程
:指计算机中已运行的程序
,程序
是 指令和数据及其组织形式的描述。在以前的描述描述中通常会有进程是操作系统资源调度的基本单位
,但在现在的操作系统中,进程不再是执行的基本单位,而是线程的容器
。相较于线程
和协程
,进程
拥有最多的资源,比如文件
、内存
等 -
线程
:是 操作系统资源调度的基本单位。一个线程是一个进程中的一个单一执行流。统一进程中的线程共享该进程的所有资源 -
协程
:又称为 用户态线程,用于完成协作式多任务
的子程序
,允许执行被挂起与被恢复。其本质还是 子例程的上下文。
协程可以这样通俗的理解,即 多个函数可以在一个线程中被平等调用,这里的 平等调用 是允许 挂起和恢复,不是传统意义上的函数调用。也就是函数可以在执行到一半的时候,在适当的时机切出去完成其他函数,而这都是在同一个线程中完成。
需要注意的的时:
1. 通常 协程
主动在合适的时机通过主动让出的方式让其他协程得以执行
2. 如果没有主动让出并完成后退出函数。会按照调度算法调度下一个协程执行
3. 具体如何完成协程的退出需要看具体实现
使用代码说明的话,其更像是:
int func()
{
A()/B()/C()
}
上述这段奇怪的代码表示的是:
- 在
func()
中可以即执行A()
,也执行B()
和C()
,他们的之间没有明确的调用顺序 - 在
A()
执行到中途时,可以在适当的时机切换为执行B()
或C()
- 当
B()
或C()
执行完成或者再某个适当的时机再切换为执行A()
-
B()
或C()
同理
以上的过程是在同一个线程中完成,不会涉及到线程的切换,减少了系统开销。
熟悉 GO语言 的读者应该对协程会比较熟悉,在 高IO 场景下,协程可以在 低系统切换开销
的情况下有效的执行多个 子程序
,从而优化整体性能。
2.3 代码示例
笔者使用一个比较简单的开源协程库进行说明,以便读者理解协程。
前提说明:
- 该代码使用
POSIX
的 ucontext函数族 进行实现,读者可以自行查阅该函数族的相关资料。 - 在该代码中可以简单理解 ucontext函数族 提供了 上下文切换的条件
下面的协程的实现讲解
/*---------------------coroutine.h---------------------*/
#ifndef C_COROUTINE_H
#define C_COROUTINE_H
#define COROUTINE_DEAD 0
#define COROUTINE_READY 1
#define COROUTINE_RUNNING 2
#define COROUTINE_SUSPEND 3
struct schedule;
typedef void (*coroutine_func)(struct schedule *, void *ud);
struct schedule * coroutine_open(void);
void coroutine_close(struct schedule *);
int coroutine_new(struct schedule *, coroutine_func, void *ud);
void coroutine_resume(struct schedule *, int id);
int coroutine_status(struct schedule *, int id);
int coroutine_running(struct schedule *);
void coroutine_yield(struct schedule *);
#endif
/*---------------------coroutine.c---------------------*/
struct coroutine;
/* 协程调度器 */
struct schedule {
char stack[STACK_SIZE]; //栈,用于协程运行时使用
ucontext_t main;//主协程
int nco;//协程数量
int cap;//协程调度器允许创建的最多协程数量
int running;//当前运行的协程id
struct coroutine **co;//数组指针,用于指向协程实例数组
};
/* 协程 */
struct coroutine {
coroutine_func func; //协程执行的子程序或函数
void *ud;//函数变量
ucontext_t ctx;//上下文
struct schedule * sch;//该协程所在的调度器
ptrdiff_t cap; //协程栈的容量
ptrdiff_t size; //协程栈的大小,一般和cap相等
int status; //协程的状态
char *stack;//协程的栈,用于协程保存栈使用,并不是再运行时使用,主要功能是保存协程的栈
};
/* 创建协程实例 */
struct coroutine *
_co_new(struct schedule *S , coroutine_func func, void *ud) {
/* 创建协程所需要的实例 */
struct coroutine * co = malloc(sizeof(*co));
/* 初始化各项基本成员 */
co->func = func;
co->ud = ud;
co->sch = S;
co->cap = 0;
co->size = 0;
co->status = COROUTINE_READY;
co->stack = NULL;
return co;
}
/* 删除协程实例 */
void
_co_delete(struct coroutine *co) {
/*
协程在调度过程中可能会开辟栈,所以这里需要释放,
相关内容需要在下面的代码中才能进行讲解
*/
free(co->stack);
/* 释放协程 */
free(co);
}
/* 打开协程调度器,通过调用该函数的为主协程 */
struct schedule *
coroutine_open(void) {
/* 开辟调度器 */
struct schedule *S = malloc(sizeof(*S));
/* 初始化调度器成员 */
S->nco = 0;
S->cap = DEFAULT_COROUTINE;//DEFAULT_COROUTINE = 16
S->running = -1;//running = -1指当前为主协程在执行
S->co = malloc(sizeof(struct coroutine *) * S->cap);//开辟协程数组
memset(S->co, 0, sizeof(struct coroutine *) * S->cap);//清空协程数组
return S;
}
/* 关闭协程调度器 */
void
coroutine_close(struct schedule *S) {
int i;
/* 删除所有协程 */
for (i=0;icap;i++) {
struct coroutine * co = S->co[i];
if (co) {
_co_delete(co);
}
}
/* 删除协程数组 */
free(S->co);
S->co = NULL;
/* 释放调度器 */
free(S);
}
/*
创建协程
S :协程调度器
func:协程需要执行的函数
ud:函数参数
*/
int
coroutine_new(struct schedule *S, coroutine_func func, void *ud) {
/* 创建协程实例 */
struct coroutine *co = _co_new(S, func , ud);
/*
查看当前协程的数量是否达到上限
如果到达上限,则扩大容量为原来的2倍,并将新创建的协程放在其中
最后返回协程的id
*/
if (S->nco >= S->cap) {
int id = S->cap;
S->co = realloc(S->co, S->cap * 2 * sizeof(struct coroutine *));
memset(S->co + S->cap , 0 , sizeof(struct coroutine *) * S->cap);
S->co[S->cap] = co;
S->cap *= 2;
++S->nco;
return id;
} else {
/*
如果协程数量没有超过上限,则将协程添加到调度器中,并返回协程的id
*/
int i;
for (i=0;icap;i++) {
int id = (i+S->nco) % S->cap;
if (S->co[id] == NULL) {
S->co[id] = co;
++S->nco;
return id;
}
}
}
assert(0);
return -1;
}
/*
协程的主要执行函数,协程并不是执行执行其函数的,一般都在mainfunc中执行
在协程执行前后需要做一些处理
low32:调度器地址的低32bit
high:调度器地址的高32bit
*/
static void
mainfunc(uint32_t low32, uint32_t hi32) {
/* 获取调度器 */
uintptr_t ptr = (uintptr_t)low32 | ((uintptr_t)hi32 << 32);
struct schedule *S = (struct schedule *)ptr;
/* 获取当前调度需要执行的协程id,并通过id获取协程的实例 */
int id = S->running;
struct coroutine *C = S->co[id];
/* 执行协程的函数 */
C->func(S,C->ud);
/* 由于协程没有主动让出并退出了函数,这里需要对协程进行回收 */
_co_delete(C);//删除协程实例
S->co[id] = NULL;//将协程在调度器中去掉
--S->nco;//减少调度器的协程数量
/*
1. 使用ucontext函数族实现时上文下切换,当前上下文退出后会切换到指定的上下文
2. 该调度器的实现是需要从主线程切换到其他协程,所以当协程退出后会返回主协程
3. 将当前执行的协程id修改为-1,表示当前是主协程执行
*/
S->running = -1;
}
/*
恢复协程,该函数通常在主协程中使用
S:协程调度器
id:需要恢复执行的协程id
*/
void
coroutine_resume(struct schedule * S, int id) {
assert(S->running == -1);
assert(id >=0 && id < S->cap);
/* 获取需要恢复执行的协程的id并判断其有效性 */
struct coroutine *C = S->co[id];
if (C == NULL)
return;
/* 获取协程的状态,并根据状态使用不同的操作 */
int status = C->status;
switch(status) {
/*
协程完成创建时状态为COROUTINE_READY
此时协程的上下文并没有完成初始化,所以需要特殊处理
*/
case COROUTINE_READY:
/* 初始化协程上下文 */
getcontext(&C->ctx);
C->ctx.uc_stack.ss_sp = S->stack;//指定协程的栈为使用调度的栈成员
C->ctx.uc_stack.ss_size = STACK_SIZE;//指定协程栈的大小
C->ctx.uc_link = &S->main;//指定协程退出后返回到主协程,与mainfunc中的操作相呼应
S->running = id;//声明当前执行的协程id
C->status = COROUTINE_RUNNING;//将协程的状态设置为RUNNING
/*
使用makecontext创建协程上下文
1. 协程的执行函数为mainfunc
2. 指定mainfunc的参数的数量为2,分别为调度器的指针的高低32bit
*/
uintptr_t ptr = (uintptr_t)S;//
makecontext(&C->ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32));
/*
使用swapcontext完成上下文的切换
1. 该函数会将当前的上下文保存在S->main中,该函数通常在主线程中执行,所以S->main也就保存了当前的上下文
2. 将当前的上下文切换到C->ctx,即刚刚构造的上下文中
3. 下一步会跳到刚刚指定的mainfunc中执行协程的func成员
*/
swapcontext(&S->main, &C->ctx);
break;
/*
协程在挂起时状态为COROUTINE_SUSPEND,此时协程已经有了上下文,但是被挂起,现在需要恢复执行
*/
case COROUTINE_SUSPEND:
/* 将协程的保存的栈设置到调度器S的栈成员中,因为在协程上下文初始化时指定的栈是调度器S的stack成员 */
memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size);
S->running = id;//声明当前执行的协程id为恢复执行协程的id
C->status = COROUTINE_RUNNING;//将协程的状态设置为RUNNING
swapcontext(&S->main, &C->ctx);//切换协程
break;
default:
assert(0);
}
}
/*
保存协程的栈,通常在协程的让出函数中调用,用于保存协程当前执行的栈
C:协程
top:协程的栈底指正,使用top表示的是地址的top,因为栈是向上增长的,所以方向反了
*/
static void
_save_stack(struct coroutine *C, char *top) {
/* dummp为当前协程的栈顶变量,通过dummu可以获取当前的栈顶指针 */
char dummy = 0;
assert(top - &dummy <= STACK_SIZE);
/* 如果当前的栈顶是否已经超出当前栈的容量 */
if (C->cap < top - &dummy) {
free(C->stack);//释放当前协程的保存栈
C->cap = top-&dummy;//获取当前栈使用的大小
C->stack = malloc(C->cap);//根据大小重新分配保存栈
}
C->size = top - &dummy;//设置保存栈大小,也就是当前栈使用的情况
memcpy(C->stack, &dummy, C->size);//将栈保存到协程的stack成员中,在这里完成了栈的保存
}
/*
协程让出函数,通常协程自己调用,用于当前协程让出给其他协程使用,在该实现中是让出到主协程
S:协程调度器
*/
void
coroutine_yield(struct schedule * S) {
/* 获取当前运行的协程id */
int id = S->running;
assert(id >= 0);
/* 获取当前协程实例 */
struct coroutine * C = S->co[id];
assert((char *)&C > S->stack);
/* 将当前的运行栈保存到协程中 */
_save_stack(C,S->stack + STACK_SIZE);
/* 将协程状态置为挂起 */
C->status = COROUTINE_SUSPEND;
/* 声明运行协程id */
S->running = -1;
/* 切换上下文到主协程 */
swapcontext(&C->ctx , &S->main);
}
/* 获取指定id协程的状态 */
int
coroutine_status(struct schedule * S, int id) {
assert(id>=0 && id < S->cap);
if (S->co[id] == NULL) {
return COROUTINE_DEAD;
}
return S->co[id]->status;
}
/* 获取当前运行的协程id*/
int
coroutine_running(struct schedule * S) {
return S->running;
}
上面主要讲解了协程的实现,但关看实现可能还无法完全了解协程是如何运行的,此时还无法将协程的整个声明周期组织起来
下面讲解一下该协程库的demo,方便读者理解
#include "coroutine.h"
#include
struct args {
int n;
};
/* 协程的执行函数 */
static void
foo(struct schedule * S, void *ud) {
struct args * arg = ud;
int start = arg->n;
int i;
for (i=0;i<5;i++) {
/* 打印当前协程的id和变量 */
printf("coroutine %d : %d\n",coroutine_running(S) , start + i);
/* 让出协程 */
coroutine_yield(S);
}
}
/* 测试函数 */
static void
test(struct schedule *S) {
struct args arg1 = { 0 };
struct args arg2 = { 100 };
/*
1. 创建2个协程,并传入相同的执行函数
2. 传入不同的变量,用于区分协程
*/
int co1 = coroutine_new(S, foo, &arg1);
int co2 = coroutine_new(S, foo, &arg2);
printf("main start\n");
/*
1. 判断2个协程的状态,如果为COROUTINE_DEAD(0)则说明协程已经退出
2. 至于协程退出可以看上一章节中的mainfunc函数讲解
*/
while (coroutine_status(S,co1) && coroutine_status(S,co2)) {
/* 如果没有退出则恢复当前协程执行 */
printf("step 1\n");
coroutine_resume(S,co1);
printf("step 2\n");
coroutine_resume(S,co2);
printf("step 3\n");
}
printf("main end\n");
}
int
main() {
struct schedule * S = coroutine_open();
test(S);
coroutine_close(S);
return 0;
}
执行结果为:
从demo可以看出几点:
1. 每次执行完协程后都需要返回主协程,并由主协程继续恢复下一个协程的运行
2. 从而可以看出该库的协程的顺序是明确的
2.4 代码修改
在 2.3章节 中我们可以了解到协程的的运行,但该实现会造成2个问题:
1.每次协程让出都需要回到主协程才能继续下一个协程,造成不必要的切换,因为主协程本身没有做任何事情
2. 协程的执行顺序由主协程指定,无法根据调度算法进行切换
基于上面2个原因,笔者修改代码以实现下面2点:
1. 协程让出或者退出后不回到主协程,由调度器查找下一个运行的协程并执行
2. 运行的协程有调度算法决定
下面的代码修改和讲解
/*---------------------coroutine_v2.h---------------------*/
#ifndef C_COROUTINE_H
#define C_COROUTINE_H
#define COROUTINE_DEAD 0
#define COROUTINE_READY 1
#define COROUTINE_RUNNING 2
#define COROUTINE_SUSPEND 3
struct schedule;
typedef void (*coroutine_func)(struct schedule *, void *ud);
struct schedule * coroutine_open(void);
void coroutine_close(struct schedule *);
/* 相比于上面版本,少了resum恢复函数,只有yield让出函数 */
int coroutine_new(struct schedule *, coroutine_func, void *ud);
int coroutine_status(struct schedule *, int id);
int coroutine_running(struct schedule *);
void coroutine_yield(struct schedule *);
#endif
/*---------------------coroutine_v2.c---------------------*/
#include "coroutine.h"
#include
#include
#include
#include
#include
#include
#if __APPLE__ && __MACH__
#include
#else
#include
#endif
#define STACK_SIZE (1024*1024)
#define DEFAULT_COROUTINE 16
/* 协程 */
struct coroutine
{
coroutine_func func;//协程执行函数
void *ud;//函数变量
ucontext_t ctx;//协程上下文
struct schedule * sch;//协程所在的调度器
ptrdiff_t cap;//协程栈的容量
ptrdiff_t size;//协程栈的大小,一般于cap相等
int status;//协程状态
char* stack;//协程的栈,该栈是运行时使用。并不是之前是保存时使用
};
/*
协程调度器
可以看出和之前相比少了许多成员
*/
struct schedule
{
int nco;//协程数量
int cap;//调度器的协程最大数量
int running;//当前运行的协程id
ucontext_t main;//主协程上下文
struct coroutine **co;//数组指针,指向协程数组
};
/* 删除协程 */
void co_delete(struct coroutine *co)
{
/*
1. 与之前的实现不同,这里删除协程修改为将协程的状态置为COROUTINE_DEAD
2. 使用该状态以让协程在调度时不会被选择
*/
co->status = COROUTINE_DEAD;
}
/* 查找下一个可以执行的协程 */
static int find_next_co(struct schedule *S, int id)
{
/*
1. 找到下一个存在切状态为挂起或者ready的协程
2. 状态为COROUTINE_DEAD的协程不会被查找到
*/
for (int i = id; i < S->nco; i++)
{
if (NULL == S->co[i])
{
continue;
}
if (COROUTINE_SUSPEND == S->co[i]->status || COROUTINE_READY == S->co[i]->status)
{
return i;
}
}
/* 如果没有满足的协程的返回-1,回到主协程 */
return -1;
}
/*
协程的主要执行函数,在该函数中执行协程中的func成员
*/
static void mainfunc(uint32_t low32, uint32_t hi32)
{
uintptr_t ptr = (uintptr_t)low32 | ((uintptr_t)hi32 << 32);
struct schedule *S = (struct schedule *)ptr;
int id = S->running;
struct coroutine *C = S->co[id];
struct coroutine *next_C = NULL;
C->func(S,C->ud);
/*
与之前的实现不同,这里对于协程退出的处理做了修改
1. co_delete没有直接删除协程,只是修改了状态
2. 因为这里还在协程的上下文中,此时删除协程会出错
3. 查找下一个执行的协程
4. 如果协程调度器没有需要执行的协程了,则直接返回主协程
*/
co_delete(C);
id = find_next_co(S, id);
if (-1 == id)
{
/* 如果协程调度器没有需要执行的协程了,则直接返回主协程 */
S->running = -1;
setcontext(&S->main);
}
else
{
/* 执行下一个协程 */
S->running = id;
next_C = S->co[id];
setcontext(&next_C->ctx);
}
}
struct coroutine *co_new(struct schedule *S , coroutine_func func, void *ud)
{
struct coroutine* co = malloc(sizeof(*co));
co->func = func;
co->ud = ud;
co->sch = S;
co->cap = STACK_SIZE;
co->size = STACK_SIZE;
co->status = COROUTINE_READY;
co->stack = malloc(STACK_SIZE);//分配协程的栈,这里主要是用于协程执行时使用
/*
初始化协程上下文
与之前相比:
1. 协程上下文的初始化在创建协程时完成,不在resum函数中执行
2. 协程上下文中运行的栈改为协程的stack成员,而不是之前调度器中stack成员
这样做的好处是不需要在调度时保存栈,直接切换上下文即可
*/
getcontext(&co->ctx);
co->ctx.uc_stack.ss_sp = co->stack;
co->ctx.uc_stack.ss_size = STACK_SIZE;
co->ctx.uc_link = &S->main;
/* 创建协程上下文 */
uintptr_t ptr = (uintptr_t)S;
makecontext(&co->ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32));
return co;
}
/* 打开协程调度器,与之前的实现一样 */
struct schedule* coroutine_open(void)
{
struct schedule *S = malloc(sizeof(*S));
S->nco = 0;
S->cap = DEFAULT_COROUTINE;
S->running = -1;
S->co = malloc(sizeof(struct coroutine *) * S->cap);
memset(S->co, 0, sizeof(struct coroutine *) * S->cap);
return S;
}
/* 关闭协程调度器 */
void coroutine_close(struct schedule *S)
{
int i;
for (i = 0; i < S->cap; i++)
{
struct coroutine* co = S->co[i];
if (co)
{
/* 删除协程及其栈 */
free(co->stack);
free(co);
}
}
free(S->co);
S->co = NULL;
free(S);
}
/* 创建协程,与之前的实现一样 */
int coroutine_new(struct schedule *S, coroutine_func func, void *ud)
{
struct coroutine *co = co_new(S, func , ud);
if (S->nco >= S->cap)
{
int id = S->cap;
S->co = realloc(S->co, S->cap * 2 * sizeof(struct coroutine *));
memset(S->co + S->cap , 0 , sizeof(struct coroutine *) * S->cap);
S->co[S->cap] = co;
S->cap *= 2;
++S->nco;
return id;
}
else
{
int i;
for (i=0;icap;i++)
{
int id = (i+S->nco) % S->cap;
if (S->co[id] == NULL)
{
S->co[id] = co;
++S->nco;
return id;
}
}
}
return -1;
}
/*
协程让出函数
1. 与之前的实现不同,这里的yield函数会指定下一个执行的协程
2. 完成协程的直接切换,不需要回到主协程
*/
void coroutine_yield(struct schedule * S)
{
int id = S->running;//获取当前执行协程的id
struct coroutine *C = NULL;
struct coroutine* next_C = NULL;
if (0 >= S->nco)
{
printf("no routine create\n");
return;
}
/* 如果当前为主协程 */
if (-1 >= id)
{
id = 0;//指定默认协程
id = find_next_co(S, id);//查找下一个可用
if (-1 == id)
{
/*
如果找不到则返回,因为当前是主协程,所以不需要切换
*/
printf("yidld error\n");
return;
}
C = S->co[id];//获取协程的实例
S->running = id;
C->status = COROUTINE_RUNNING;
swapcontext(&S->main, &C->ctx);//切换到第一个协程
}
else //如果当前为其他协程
{
C = S->co[id];//获取当前协程
C->status = COROUTINE_SUSPEND;//将当前协程的状态修改为挂起
id = find_next_co(S, id);//查找下一个可用
if (-1 == id)
{
/*
如果找不到可执行协程,则返回主协程
*/
setcontext(&S->main);
}
next_C = S->co[id];//获取下一个协程实例
next_C->status = COROUTINE_RUNNING;//将下一个协程状态置为运行
S->running = id;//设置运行协程的id
swapcontext(&C->ctx , &next_C->ctx);//切换到下一个协程
}
}
/* 获取指定协程状态 */
int coroutine_status(struct schedule * S, int id)
{
if (S->co[id] == NULL)
{
return COROUTINE_DEAD;
}
return S->co[id]->status;
}
/* 获取当前运行的协程 */
int coroutine_running(struct schedule * S)
{
return S->running;
}
上面讲解修改后的代码,下面我们看看具体的使用用例方便读者理解
#include "coroutine_v2.h"
#include
struct args {
int n;
};
static void
foo(struct schedule * S, void *ud) {
struct args * arg = ud;
int start = arg->n;
int i;
for (i=0;i<5;i++) {
printf("coroutine %d : %d\n",coroutine_running(S) , start + i);
coroutine_yield(S);
}
}
static void
test(struct schedule *S) {
struct args arg1 = { 0 };
struct args arg2 = { 100 };
/* 创建2个协程 */
int co1 = coroutine_new(S, foo, &arg1);
int co2 = coroutine_new(S, foo, &arg2);
printf("main start\n");
/* 判断协程状态是否正常 */
while (0 != coroutine_status(S,co1) || 0 != coroutine_status(S,co2)) {
/* 让出协程 */
printf("before yield\n");
coroutine_yield(S);
printf("after yield\n");
}
printf("main end\n");
}
int
main() {
struct schedule * S = coroutine_open();
test(S);
coroutine_close(S);
return 0;
}
执行结果如下,可以看到每次调度都没有进入主协程
PS:以上代码作为demo进行,可能存在bug,欢迎各位读者朋友讨论
2.5 总结
总结一下,协程有以下优点及适用场景如下:
优点:
- 在某些场景如生产消费场景,可以简化编程模型
- 降低切换开销,因为不需要系统中断和系统进行调度,仅有上下文开销
适用于 高IO场景,典型的有:
- 高并发网络编程
- 高IO,比如 异构计算、磁盘IO、设备IO
三、参考链接
- 参考代码
- 维基百科
- 协程 以及进程和线程
- 协程(coroutine)简介
- 上下文切换