Coroutine及其实现
线程是内核对外提供的服务,应用程序可以通过系统调用让内核启动线程,由内核来负责线程调度和切换。线程在等待IO操作时线程变为unrunnable状态会触发上下文切换。现代操作系统一般都采用抢占式调度,上下文切换一般发生在时钟中断和系统调用返回前,调度器计算当前线程的时间片,如果需要切换就从运行队列中选出一个目标线程,保存当前线程的环境,并且恢复目标线程的运行环境,最典型的就是切换ESP指向目标线程内核堆栈,将EIP指向目标线程上次被调度出时的指令地址。
协程也叫用户态线程,协程之间的切换发生在用户态。在用户态没有时钟中断,系统调用等机制,那么协程切换由什么触发?调度器将控制权交给某个协程后,控制权什么时候回到调度器,从而调度另外一个协程运行? 实际上,这需要协程主动放弃CPU,控制权回到调度器,从而调度另外一个协程运行。所谓协作式线程(cooperative),需要协程之间互相协作,不需要使用CPU时将CPU主动让出。
协程切换和内核线程的上下文切换相同,也需要有机制来保存当前上下文,恢复目标上下文。在POSIX系统上,getcontext/makecontext/swapcontext等可以用来做这件事。
协程带来的最大的好处就是可以用同步的方式来写异步的程序。比如协程A,B:A是工作协程,B是网络IO协程(这种模型下,实际工作协程会比网络IO协程多),A发送一个包时只需要将包push到A和B之间的一个channel,然后就可以主动放弃CPU,让出CPU给其它协程运行,B从channel中pop出将要发送的包,接收到包响应后,将结果放到A能拿到的地方,然后将A标识为ready状态,放入可运行队列等待调度,A下次被调度器调度就可以拿到结果继续做后面的事情。如果是基于线程的模型,A和B都是线程,通常基于回调的方式,1. A阻塞在某个队列Q上,B接受到响应包回调A传给B的回调函数f,回调函数f将响应包push到Q中,A可以取到响应包继续干活,如果阻塞基于cond等机制,则会被OS调度出去,如果忙等,则耗费CPU。2. A可以不阻塞在Q上,而是继续做别的事情,可以定期过来取结果。 这种情况下,线程模型业务逻辑其实被打乱了,发包和取包响应的过程被隔离开了。
实现协程库的基本思路很简单,每个线程一个调度器,就是一个循环,不断的从可运行队列中取出协程,并且利用swapcontext恢复协程的上下文从而继续执行协程。当一个协程放弃CPU时,通过swapcontext恢复调度器上下文从而将控制权归还给调度器,调度器从可运行队列选择下一个协程。每个协程初始化通过getcontext和makecontext,需要的栈空间从堆上分配即可。
以下分析一个简单的协程库libtask,由golang team成员之一的Russ cox在加入golang team之前开发。只支持单线程,简单包装了一下read/write等同步IO。
在libtask中,一个协程用一个struct Task来表示:
struct Task
{
char name[]; // offset known to acid
char state[];
Task *next; //通过这两个指针将task串起来
Task *prev;
Task *allnext;
Task *allprev;
Context context;// 当前协程上下文
uvlong alarmtime;
uint id;
uchar *stk; // 当前协程可以使用的堆栈,初始化为栈顶地址
uint stksize;// 当前协程可以使用的堆栈大小
int exiting;
int alltaskslot;
int system;
int ready;
void (*startfn)(void*);//当前协程的执行入口函数
void *startarg;//参数
void *udata;
};
下面看看新增一个协程的过程:
static Task*
taskalloc(void (*fn)(void*), void *arg, uint stack)
{
Task *t;
sigset_t zero;
uint x, y;
ulong z; /* allocate the task and stack together */
t = malloc(sizeof *t+stack); //在堆上为这个协程分配结构体和协程所使用的堆栈
if(t == nil){
fprint(, "taskalloc malloc: %r\n");
abort();
}
memset(t, , sizeof *t);
t->stk = (uchar*)(t+);
t->stksize = stack;
t->id = ++taskidgen;
t->startfn = fn; // 协程入口函数
t->startarg = arg; // 协程入口函数参数 /* do a reasonable initialization */
memset(&t->context.uc, , sizeof t->context.uc);
sigemptyset(&zero);
sigprocmask(SIG_BLOCK, &zero, &t->context.uc.uc_sigmask); /* must initialize with current context */
if(getcontext(&t->context.uc) < ){ // 初始化当前协程上下文
fprint(, "getcontext: %r\n");
abort();
} /* call makecontext to do the real work. */
/* leave a few words open on both ends */
t->context.uc.uc_stack.ss_sp = t->stk+; //ss_sp成员为栈顶地址,后续makecontext会将ss_sp往高地址移动ss_size个字节,从这里开始压栈
t->context.uc.uc_stack.ss_size = t->stksize-; //ss_size成员为栈大小
#if defined(__sun__) && !defined(__MAKECONTEXT_V2_SOURCE) /* sigh */
#warning "doing sun thing"
/* can avoid this with __MAKECONTEXT_V2_SOURCE but only on SunOS 5.9 */
t->context.uc.uc_stack.ss_sp =
(char*)t->context.uc.uc_stack.ss_sp
+t->context.uc.uc_stack.ss_size;
#endif
/*
* All this magic is because you have to pass makecontext a
* function that takes some number of word-sized variables,
* and on 64-bit machines pointers are bigger than words.
*/
//print("make %p\n", t);
z = (ulong)t;
y = z;
z >>= ; /* hide undefined 32-bit shift from 32-bit compilers */
x = z>>;
makecontext(&t->context.uc, (void(*)())taskstart, , y, x); // 协程入口函数为taskstart,y,x两个参数会被压到t->context.uc.uc_stack栈底
return t;
}
然后调用taskready将这个协程放入可运行队列中:
void
taskready(Task *t)
{
t->ready = ; //
addtask(&taskrunqueue, t); //将协程放入到可运行队列中,后续调度器就可以从taskrunqueue中拿到它了。taskrunqueue就是一个全局变量,libtask只支持单线程从这里也可以看出来
}
现在可以看看调度器:
static void
taskscheduler(void)
{
int i;
Task *t; taskdebug("scheduler enter");
for(;;){ //无限循环
if(taskcount == )
exit(taskexitval);
t = taskrunqueue.head; //从可运行队列头部取出下一个运行的协程
if(t == nil){
fprint(, "no runnable tasks! %d tasks stalled\n", taskcount);
exit();
}
deltask(&taskrunqueue, t); //从可运行队列中将它删除
t->ready = ;
taskrunning = t; //将t设置为当前正在运行的协程,taskrunning是一个全局变量
tasknswitch++; //统计值,协程一共执行了多少次
taskdebug("run %d (%s)", t->id, t->name);
contextswitch(&taskschedcontext, &t->context); // 通过swapcontext切换到目标协程,并且将调度器上下文保存在全局变量taskschedcontext中
//print("back in scheduler\n"); taskrunning = nil; if(t->exiting){ if(!t->system) taskcount--; i = t->alltaskslot; alltask[i] = alltask[--nalltask]; alltask[i]->alltaskslot = i; free(t); } } }
协程主动放弃CPU调用taskyield:
int
taskyield(void)
{
int n;
n = tasknswitch;
taskready(taskrunning); // 将自己设置为ready重新放回可运行队列
taskstate("yield");
taskswitch(); //将控制权还给调度器
return tasknswitch - n - ;
}
看看taskswitch:
void
taskswitch(void)
{
needstack(); // 检查当前协程是否堆栈溢出,如果溢出,程序退出
contextswitch(&taskrunning->context, &taskschedcontext); // 切换到 taskschedcontext 上下文,从上面调度器循环可以看出,它就是调度器上下文
}
看看如何检查协程堆栈溢出:
void
needstack(int n)
{
Task *t;
t = taskrunning; // t是个栈变量,当前协程是taskrunning
if((char*)&t <= (char*)t->stk // t是taskrunning, stk是taskrunning这个协程的栈顶,栈的增长方向是从高到低,stk是低地址,显然,t这个局部变量的地址小于stk时,栈溢出
|| (char*)&t - (char*)t->stk < +n){ // 如果离stk的地址小于256+n,则同样说明溢出,为什么这里需要预留256+n,不太清楚。
fprint(, "task stack overflow: &t=%p tstk=%p n=%d\n", &t, t->stk, +n);
abort();
}
}
最后看看contextswitch:
static void
contextswitch(Context *from, Context *to)
{
if(swapcontext(&from->uc, &to->uc) < ){ //调用swapcontext切换到to->uc协程
fprint(, "swapcontext failed: %r\n");
assert();
}
}
taskswitch之后控制权回到调度器,调度器就继续从可运行队列中取出下一个协程运行了。
下面看看makecontext:
void
makecontext(ucontext_t *ucp, void (*func)(void), int argc, ...)
{
int *sp; sp = (int*)ucp->uc_stack.ss_sp+ucp->uc_stack.ss_size/; // 将sp移动到分配的栈空间的最高地址
sp -= argc; // 往栈低地址方向留出argc个sizeof(int)空间用于后续压argc个int参数进栈
sp = (void*)((uintptr_t)sp - (uintptr_t)sp%); /* 16-align for OS X */
memmove(sp, &argc+, argc*sizeof(int)); //将argc后面的int参数进栈 *--sp = ; /* return address */ // 函数返回后执行的下一条指令,这个返回值没用,因为协程是由外部调度器调度的。
ucp->uc_mcontext.mc_eip = (long)func; //设置IP
ucp->uc_mcontext.mc_esp = (int)sp; //设置当前栈顶,告诉func从哪里分配栈变量
}
由于函数调用返回,压栈顺序,栈帧的变化参看:http://www.cnblogs.com/foxmailed/archive/2013/01/29/2881402.html
以上就是协程相关的全部流程。
后续分析同步IO操作的封装。
Coroutine及其实现的更多相关文章
- Coroutine in Java - Quasar Fiber实现--转载
转自 https://segmentfault.com/a/1190000006079389?from=groupmessage&isappinstalled=0 简介 说到协程(Corout ...
- The Coroutine
关于Coroutine 说到coroutine就不的不说subroutine,也就是我们常用到的一般函数.调用一个函数开始执行,然后函数执行完成后就退出,再次调用的时候,再从头开始,调用之间是没有保存 ...
- c coroutine
今天看了下云风c coroutine 代码 博客,发现 coroutine 实现原理其实还比较简单,就用户态栈切换,只需要几十行汇编,特别轻量级. 具体实现 1. 创建一个coroutine: 也就 ...
- lua coroutine for iterator
背景 前面的文章演示了使用闭包函数实现 状态的迭代器. 本文演示使用 coroutine来产生迭代器的例子. coroutine迭代器例子 -- 遍历二叉树 local binary_tree = { ...
- python中的generator(coroutine)浅析和应用
背景知识: 在Python中一个function要运行起来,它在python VM中需要三个东西. PyCodeObject,这个保存了函数的代码 PyFunctionObject,这个代表一个虚拟机 ...
- hive源码之新建一个coroutine
最近由于项目需要读了一下云风老大的hive项目代码,因为对lua只有熟悉的水平,下面的东西必然多多错误:),只为记录. lua_State *sL = schedule_newtask(L); str ...
- Lua Coroutine详解
协同程序与线程差不多,也就是一条执行序列,拥有自己独立的栈,局部变量和指令指针,同时又与其它协同程序共享全局变量和其它大部分东西.线程与协同程序的主要区别在于,一个具有多线程的程序可以同时运行几个线程 ...
- 【Unity3D基础教程】给初学者看的Unity教程(五):详解Unity3D中的协程(Coroutine)
作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点[推荐].谢谢! 为什么需要协程 在游戏中有许多过程(Proc ...
- Lua 协程coroutine
协程和一般多线程的区别是,一般多线程由系统决定该哪个线程执行,是抢占式的,而协程是由每个线程自己决定自己什么时候不执行,并把执行权主动交给下一个线程. 协程是用户空间线程,操作系统其存在一无所知,所以 ...
- U3D中的 Coroutine程序 解析
今天咱就说说协同程序coroutine. 什么是协同程序 先说说啥是协程:它的表现形式非常像线程,对线程有过接触的朋友可能更理解我这句话的意思,你没接触过线程,那么理解它会有一些难度.但是它不存在线程 ...
随机推荐
- 解析ASP.NET WebForm和Mvc开发的区别 分类: ASP.NET 2013-12-29 01:59 11738人阅读 评论(5) 收藏
因为以前主要是做WebFrom开发,对MVC开发并没有太深入的了解.自从来到创新工场的新团队后,用的技术都是自己以前没有接触过的,比如:MVC 和EF还有就是WCF,压力一直很大.在很多问题都是不清楚 ...
- 【树】Lowest Common Ancestor of a Binary Tree(递归)
题目: Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree. Accor ...
- __name__ == "__main__"的作用是什么?
问题: __name__ == "__main__" 的作用是什么? # Threading example import time, thread def myfunction( ...
- SpringBoot入门 (一) HelloWorld
一 什么是springboot springboot是一个全新的框架,它设计的目的简化spring项目的初始环境的搭建和开发,主要有以下几个特点: 1.简化初始配置 ,可与主流框架集成: 2.内置Se ...
- 布局管理器之CardLayout(卡片布局管理器)
对于选项卡这个概念大家可能不会陌生,就是在一个窗口中可以切换显示多页不同的内容,但同一时间只能是其中的某一页可见的,这样的一个个的页面就是选项卡. CardLayout就是类似的这样一个布局管理器,它 ...
- Mongodb同步数据到hive(二)
Mongodb同步数据到hive(二) 1. 概述 上一篇文章主要介绍了mongodb-based,通过直连mongodb的方式进行数据映射来进行数据查询,但是那种方式会对线上的 ...
- [codeup] 2044 神奇的口袋
题目描述 有一个神奇的口袋,总的容积是40,用这个口袋可以变出一些物品,这些物品的总体积必须是40.John现在有n个想要得到的物品,每个物品的体积分别是a1,a2--an.John可以从这些物品中选 ...
- Python中类的属性的访问控制
因为自己是做.NET的,之前有学习过Python,喜欢这门语言的很多特性,最近又不时看了一会儿,将自己的感受分享给大家,其中也难免会用C#的角度看Python的语法,主要还是讲下Python中类中对属 ...
- 构造用于1个方法在多线程环境下重复多次执行测试的通用方法 C#
static void MultithreadsCycleTestWithSameMethod(int threadNumber, int cycleNumber, ThreadStart actio ...
- Form表单和里边的小部件
一.Form表单:form表单是用来收集用户信息,并向后台提交信息的区域表单: 1.属性 “action” 是 “行为“的意思,该属性的值表示:用户提交信息到哪个页面: 2.属性”method“ 是” ...