下载  >  开发技术  >  C++  > 腾讯开源协程库libco-原理及应用.pdf

腾讯开源协程库libco-原理及应用.pdf 评分

腾讯协程库libco的原理分析及实际应用,深入分析了libco的实现方法和数据结构,经典的文档,开源值得参考
22栈的概念回顾 TBD 3 Tibco使用简介 31-个简单的例子 在多线程编程教程中,有一个经典的例子:生产者消费者问题。事实上,生产者消 费者问题也是最适合协程的应用场景。那么我们就从这个简单的例子入手,来看一看 使用 tibco编写的生产者消费者程序(例程代码来自于ibco源码包) truct trask t t nt id s struct stEnv t t stcocond t* cond i queue<trask t*> task queue; 10 (voids args)t co enable hook sys()i (stEnv t*)args; int ld 0 while (true)f stTask t* task =(stTask t*)calloc(l, sizeof(stTask t)) task->id id++i k queue. push(task 18 printf( 8s: gd produce task d\n, func, LINE task->id) co cond signal(env->cond)i 11(IU 0;1000 return NULL; 25 void* Consumer(void* args)I ble hook sys() stEnv t* env =(stEnv tx)args; Thile (true)( f (env->task queue co cond timedwait(env>cond,-1); continue 32 s七 Task t*task sk queue. front() v->task .pop() printf( s: d consume task d\n, func LINE, task->id)i free(task) 37 return NULL 生产者和消费者协程 在上面的代码中, Producer与 Consumer函数分别实现了生产者与消费者的逻辑, 函数的原型跟 pthread线程函数原型也是一样的。不同的是,在函数第一行还调用了一 个 co enable hook sys0(,此外,不是用 sleep去等待,而是polO。这些原因后文会详 细解释,暂且不管。接下来我们看怎样创建和启动生产者和消费者协程 int main() stEny tx env new stEnv t. env ond co cond alloc(); stcoRoutlne t* consumer routine co create(&consumer routine, NULL, Consumer, env) 789 co resume( consumer routine stCoRoutine t* producer routine; co create(&producer routine, NULL, Producer, env) co resume( producer routine) 12 co eventloop(co get epoll ct(), NULL, NULL) return 0 创建和启动生产者消费考协程 初次接触 libo的读者,应该下载源码编译,亲自运行一下这个例子看看输出结 果是什么。实际上,这个例子的翰出结果跟多线程实现方案是相似的, Producer与 Consumer交替打印生产和消费信息。 再来看代码,在main0函数中,我们看到代表一个协程的结构叫做 coRoutine t, 创建一个协程使用 co create函数。我们注意到,这旦的 co createD的接口设计跟 pthread的 pthread created是非常相似的。跟 pthread不太一样是,创建出一个协程之后 并没有立即启动起来;这里要启动协程,还需调用 co resume (O函数。最后, pthread创建 线程之后主线程往往会 pthread join等等子线程退出,而这里的例子没有“ co Join0” 或类似的函数,而是调用了一个 co eventloopo函数,这些差异的原因我们后文会详细 解柝。 然后再看 Producer和 Consumer的实现,细心的读考可能会发现,无论是 Producer 还是 Consumer,它们在操作共享的队列时都没有加锁,没有互斥保护。那么这样做是否 安全呢?其实是安全的。在运行这个程序时,我们用ps命令会看到这个它实际上只有 个线程。囚此在任何时刻欠理器上只会有一个协程在运行,所以不存在 race conditions 不需要任何互斥保护。 还有一个问题。这个程序既然只有一个线程,那么 Producer与 Consumer这两个协 程酌数是怎样做到交替执行的呢?如果你熟悉 pthread和操作系统多线程的原理,应该 很快能发现程序里 co cond signalo、pol)和 co cond timedwait(这几个关键点。换作 是一个 pthread编写的生产者消费者程序,在只有单核CPU的机器上执行,结果是不是 一样的? 总之,这个例子跟 pthread实现的生产者消费者程序是非常相似的。通过这个例子, 我们也大致对 libo的协程接口有了初步的了解。为了能看懂本文接下来的内容,建议 把其他几个例子的代码也郡浏览一下。下文我们将不再直接列出 libo例子中的代码, 如果有引用到,请自行参看相关代码。 4 4 tibco的协程 通过上一节的例子,我们已经对 libo中的协栏有了初步的印象。我们完全可以把 它当做一种用户态线程来看待,接下来我们就从线程的角度来开始探究和理解它的实 现机制。 以 Linux为例,在操作系统提供的线程机制中,一个线程一般具备下列要素: (1)有一段程序供其执行,这个是显然是必须约。另外,不同线程可以共用同一段 程序。这个也是显然的,想想我们程序设计里经常用到的线程池、工作线程,不同的工 作线程可能执行完全一样的代码。 (2)有起码的“私有财产”,即线程专属的系统堆栈空间。 (3)有“户口”,操作系统教科书里叫做“进(线)程控制扶”,英文缩写叫PCB。在 Linux内核里,则为 task struct的一个结构体。有了这个数据结构,线程才能成为内核 凋度的一个基本单位接受内核调度。这个结构也记录着线程占有的各项资源。 此外,值得一提的是,操作系统的进程还有自己专属的内存空间(用户态内存空 间),不同进程间的内存空间是相互独立,互不干扰的。而同属一个进程的各线程,则 是共享内存空间的。显然,协程也是共享内存空间的 我们可以借鉴操作系统线程的实现慝想,在OS之上实现用户级线程(协程)。跟 OS线栏一样,用户级线程也应该具备这三个要素。所不同的只是第二点,用户纵线程 (协程)没有自已专属的堆空间,只有栈空间。首先,我们得准备一段程序供协程执行, 这即是 co create)函数在创建协程的时候传入的第三个参数—形参为void*,返回值 为void的一个函数 其次,需要为创建的协程准备一段栈内存空间。栈内存用于倸存调用函数过程中 的临时变量,以及函数调用链(栈帧)。在 Intel的x86以及ⅹ64体系结构肀,栈顶由 ESP(RSP)寄存器确定。所以一个创建一个协程,启动的时候还要将ESP(RSP)切到 分配的栈内存上,后文将对此做详细分析 co create调用成功后,将返回一个 coRoUtine t的结构指针(第一个参数)。从 命名上也可以看出来,该结构即代表了 libo的协程,记录着一个协程拥有的各和资源, 我们不妨称之为“协程控制块”。这样,构成-个协猩三要素—一执行的函数,栈内存, 协程控制块,在 co create调用完成后便都准备就绪了 5关键数据结构及其关系 I struct stCoRoutine t t stcoRoutineenv t *env pfn co routine t pfni void *arg coctx t ct char cstarti char CEnd char cIsmain 10 char cEnablesysHook i char cIssharestack void *pvEnv; //char sRunstack[ 1024 128 i 5678 ststackMem t* stack mem //save satck buffer while confilct on same stack buffer char* stack sp unsigned int save size char* save buffer stcospec t aspec[1024] libo的赤程控制块 coroutine t 接下来我们逐个来看一下 st coroutine t结构中的各项成员。首先看第2行的env, 协程执行的环境。这里提一下,不同于go语言, tibco的协程一旦刨建之后便跟创建时 的那个线程绑定了的,是不支持在不同线程间迁移( migrate)的。这个env,即同属于 一个线程所有协程的执行环境,包括了当前运行协程、上次切换挂起的协程、嵌套调用 的协程栈,和一^ epoll的封装结构(TBD)。第3、4行分别为实际待执行约协程函数 以及参数。第5行,ctx是一个 coctx t类型的结构,用于协程切换时保存CPU上下文 ( context)的;所谓的上下文,即esp、ebp、eip和其他通用寄存器的值。第7至11行是 些状态和标志变量,意义也很明了。第13行pEnv,名字看起来有点费解,我们暂 且知道这是一个用于倸存程序系统环境变量的指针就好了。16行这个 stack men,协 程运行时的栈内存。通过注释我们知道这个栈内存是固定的128KB的大小。我们可以 计算一下,每个协程128K内存,那么一个进程启100万个协程则需要占用高达12GB 的内存。读者大概会怀疑,不是常听说协程很轻量级吗,怎么会占用这么多的内存?答 案就在接下来19至21行的几个成员变量中。这里要提到实现 sackful协程(与之相 对的还有一种 stackless协程)的两种技术: Separate coroutine stacks和 Copying the stack (又叫共亨栈)。实现细节上,前者为每一个协程分配一个单独的、固定大小的栈;而后 者则仅为正在运行的协程分配栈内存,当协程被调度切换出去时,就把它实际占用的 栈内存copy俣存到一个单独分配的缓冲区;当被切出去的协程再次调度扶行时,再 次copy将原来俣存的栈内存恢复到那个共享的、固定大小的栈内存空间。通常情况下, 一个协程实际占用的(从esp到栈底)栈空间,相比预分配的这个栈大小(比如 libo 的128KB)会小得多;这样一来, copying stack的实现方案所占用的内存便会少很多。 当然,协程切换时拷贝内存的开销有些场景下也是很大的。因此两种方案冬有利弊,而 libo则同时实现了两种方案,默认使用前者,也允许用户在创建协程时指定使用共烹 栈 1 struct coc七xt{ 2 #if defined( 1386) void *regs[8]; 4#e⊥se egs[14] 并 endif size t ss size char *ss spi 6 用于保存协程执行上下文的 coctx t结构 前文还提到,协程控制块 coRoutine t结构甲第一个字段env,用于保存协程的运 行“环境”。前文也指出,这个结构是跟运行的线程绑定了的,运行在同一个线程上的 各协程是共享该结构的,是个全局性的资源。那么这个 coRoUtine eny t到底包含什么 重要信息呢?请看代码: I struct stCoRoutineenv t i 2 stcoRoutine t *callsTack[1281i int icallStacksize stcoEpoll t *pEpoli // for copy stack log lastco and nextco 7 stcoRoutine_t* pending_co; stcoRoutine tk ocupy co; y}; 协程的 stCoroutine eny t结构 我们看到 stCoRoutine eny t内部有一个叫做Cal! Stack的“栈”,还有个 scOpOli t结构 指针。此外,还有两个 st Coroutine t指针用于记录协程切换时占有共享栈的和将要切 换运行的协程。在不使用共享栈模式时 pending co和 occupy co都是空指针,我们暂且 忽略它们,等到分析共享栈的时候再说 stCoRoutineeny t结构里的 cAllsTack不是普通意义上我们讲的那个程序运行栈, 那^ESP(RSP)寄存器指向的栈,是用来保留浧序运行过程中局部变量以及函数调用 关系的。但是,这个 p CallStack又跟LSP(RSP)指向的栈有相似之处。如果将协程看 成一种特殊的函数,那么这个 cAllsTack就时保存这些函数的调用链的栈。我们已经讲 过,非对称协栏最大特点就是协程间存在明确的调用关系;甚至在有些文畎中,启动协 程被称作cll,挂起协程叫 return。非对称协程机制下的被调协程只能返冋到调用者协 程,这种调用关系不能乱,因此必须将调用链保存下来。这即是 cAllsTack的作用,将 它命名为“调用栈”实在是恰如其分。 每当启动( resume)一个协程时,就将它的协程控制块 coRoutine t结构指针保存 在 p CallStack的“栈顶”,然后“栈指针” cAllsTack size加1,最后切换 context到待启 动协程运行。当协程要让出( yield)CPU时,就将它的 coroutine t从pCal丨tack弹 出,“栈指针” cAllstack size减1,然后切换 context到当前栈顶的协程(原来被挂起的 调用者)恢复执行。这个“压栈”和“弹栈”的过程我们在 co resumed和 co yicld函 数中将会再次讲到。 那么这里有一个问题, libo程序的第一个协程呢,假如第一个协程 yicld时,CPU 控制权让给谁呢?关于这个问题,我们首先要明白这“第一个”协程是什么。实际上, libo的第一个协程,即执行main函数的协程,是一个特殊的协程。这个协程又可以称 作主协稈,它负责协调其他协程的调度执行(后文我们会看到,还有网络IO以及定时 亨件的驱动),它自己则永远不会 yicld,不会主动让出CPU。不让出(yild)CPU,不 等于说它一直霸占着CPU。我们知道CPU扶行权有两和转移途径,一是通过 yicld让给 凋用者,其二则是 resume启动其他协稈运行。后文我们可以清楚地看到, co resume 7 与 co yield都伴随着上下文切换,即CPU控制流的转移。当你在程序中筼一次调用 co resumed时,CPU执行权就从主协程转移到了 resume目祘协程上了。 提到主协程,那么另外一个问题又来了,主协程是在什么时候创建出来的呢?什么 时候 resume的?事实上,主协程是跟 stcoroutine eny t一起创建的。主协程也无需 调用 resume来启动,它就是程序本身,就是main函数。主协程是一个特殊的存在,可 以认为它只是一个结构体而已。在程序首次调用 co create()时,此函数内部会判断当前 进程(线程)的 stCoroutineeny t结构是否已分配,如果未分配则分配一个,同时分配 stcoroutine t结构,并将 cAllsTack[0]指向主协程。此后如果用 co resumed启动 协程,又会将 resume的协程压入 cAllsTack栈。以上整个过程可以用图1来表示。 coroutine 2 co routine l 主协程 cAllsTack 图1: stCoroutine eny t结构的 p CallStack示意图 在图1中, coroutine2整处于栈顶,也即是说,当前正在CPU上 running的协程是 coroutine2。而 coroutine2的调用者是谁呢?是谁 resume了 coroutine2呢?是 coroutine l。 coroutine l则是主协程启动的,即在main函数里 resume的。当 coroutine2让出CPU时, 只能让给 coroutine;如果 coroutine再让岀CPU,那么又回到了主协程的控制流上了 当控制流回到主协程上时,主协程在干些什么呢?回过头来看生产者消费考那个 例子。那个例子中,main函数中程序最终调用了 co eventloop(。该函数是一个基于 epoll/kqueue的事件循环,负责调度其他协程运行,具伓细节暂时略去。这里我们只需 知道, stcoroutineeny t结构中约 pepoli即使在这里用的就够了。 至此,我们已经基本理解了 stCo Routine eny t结构的作用。待衤充。 6 Tibco协程的生命周期 61创建协程( Creating coroutines 前文已提到, libo中创建协程是 co create(函数。函数声明如下 I int co create(stcoRoutine tx* co, const stcoRoutineAttr t* attr, void*(*routine) void*), void* arg) 同 pthread create一样,该函数有四个参数 @co: coRoutine t*类型的指针。输出参数, co create内部会为新协程分配一个 协裎控制块”,*co将指向这个分配的协程控制块。 ar: coRoutine∧trt*类型的指针。输入参数,用于指定要创建协程的属性,可 为NUIL。实际上仅有两个属性:栈大小、指向共享栈的指针(使用共享栈模式)。 routine:void*(*)(void*)类型的函数指针,指向协程的任务函数,即启动这个协 程后要完成什么样的任务。 routine类型为函数指针。 8 arg;:void类型指针,传递给任务函数的参数,类似于 pthread传递给线程的参数 调用 co create将协程创建出来后,这时侯它还没有启动,也即是说我们传递的 routine函数还没有被调用。实质上,这个酌数内部仅仅是分配并初始化 coRoutine t 结构体、设置任务函数指针、分配一段“栈”内存,以及分配和初始化 cost t为什么 这里的“栈”要加个引号呢?因为这里的栈内存,无论是使用预先分配的共享栈,还是 co create内部单独分配的栈,其实都是调用 malloc从进程的堆为存分配岀来的。对于 协程而言,这就是“栈”,而对于底层的进程(线程)来说这只不过是普通的堆内存而 总体上, co create函数内部做的工作很简单,这里就不贴出代码了。 62启动协程( Resume a coroutine) 在调用 co create创建协程返回成功后,便可以调用 co resume函数洊它启动了。该 函数声明如下 void co_resume(stCoRoutine_t* co); 它的意义很明了,即启动co指针指向的协程。值得注意的是,为什么这个函数不 aL co start而是 co resume呢?前文已提到, libo的协程是非对称协程,协程在让出 CPU后要恢复执行的时候,还是要再次调用一下 co resume这个函数的去“启动”协程 运行的。从语义上来讲, co start只有一次,而 co resume可以是暂停之后恢复启动,可 以多次调用,就这么个区别。实际上,看早期关于协程的文献,讲到非对称协程,一般 乜都用“ resume”与“ yield”这两个术语。协程要获得CPU执行权用“ resume",而让 出CPU扶行权用“ yield”,这是两个是两个不同的(不对称的)过程,因此这种机制才 被称为非对称协程( asymmetric coroutines)o 所以讲到 resume一个协程,我们一定得注意,这可能是第一次启动该协程,也可 以是要准备重新迈行挂起的协程。我们可以认为在lbco里面协程只有两种状态,即 running和 pending。当创建一个协程并调用 resume之后便进入了 running状态,之后协 程可能通过 yield让出CPU,这就进入了 pending状态。不断在这两个状态间循环往复, 直到协程退出(执行的任务函数 routine返回),如图2所示(TBD修改状态机)。 ield reac y running pending exit resume 图2:对称协程状态转换图 需要指出的是,不同于go语言,这里 co resume(启动一个协程的含义,不是“创 建一个并发任务”。进入 co resumed函数后发生协程的上下文切换,协程的任务函数 是立即就会被执行的,而且这个执行过程不是并发的( Concurrent)。为什么不是并发的 呢?因为 co resumed函数内部会调用 coctx swapO将当前协程挂起,然后就开始执行 目标协栏的代码了(具体过程见下文协程切换那一节的分析)。本质上这个过程是串行 的,在一个操作系统线程(进程)上发生的,甚至可以说在一颗CPU核上发生的(假 定没有发生 CPU migration)。让我们站到 Knuth的角度,将 coroutine当做一种特殊的 subroutine来看,问趣会显得更清楚:A协程调用 co resume(B)启动了B协程,本质上 是一种特殊的过程调用关系,A调用B进入了B过程内部,这很显然是一种串行执行 的关系。那么,既然 co resume(调用后进λ了被调协程执行控制流,那么 co resume 函数本身何时返回?这就要等被调协程主动让出CPU了。(TDB补充图) I void co resume(stCoRoutine t *co) t stcoRoutineenv t *env co->env stCoRoutine t *lpCurrRoutine env->pCallStack[env->iCallStacksize-1li if (Ico->sTart)t coctx make(&co-ctx, (coctx pfn t)CoRoutinefunc, co, 0) co->sTart =l; env->pCallStack[env->iCallStackSize++] = co; co swap(lpCurrRoutine, co) co_resumeD函数代码实现 如果读者对 co resumed的逻辑还有疑问,不妨再看一下它的代码实现。第5、6行 的i条件分支,当且仅当协程是第一次启动时才会执行到。首次启动协程过程有点特 殊,需要调用 coctx makeO为新协程准备 context(为了让 co swapo内能珧转到协程的 任务函数),并将 sTart标志变量置1。忽略第4-7行首次启动协程的待殊逻辑,那么 co resume(仅有4行代码而已。第3行取当前协程控制块抬针,第8行将待启动的协 程co压入 p CallStack饯,然后第9行就调用 co swap(切换到co指向的新协程上去执 行了。前文也已经提到, co swapo不会就此返回,而是要这次 resume的co协程主动 vield让出CPU时才会返回到 co resumed中来。 值得指出的是,这里讲 co swapo不会就此返回,不是说这个函数就狙塞在这里等 待co这个协程 yield让出CPU。实际上,后面我们将会看到, co swap(为部已经切换 了CPU扶行上下文,奔着co协程的代码路径去执行了。壑个过程不是并发的,而是串 行的,这一点我们已经反复强调过了。 63协程的拦起( Yield to another coroutine) 在非对称协程理论,vild与 resume是个相对的操作。A协程 resume启动了B协 程,那么只有当B协程执行yeld澡作时才会返回到A协程。在上一节剖析协程启动 函数 co resume(O时,也提到了该函数内部co_ swapo会执行被调协程的代码。只有被 调协程 yield让出CPU,调用者协程的 co swapo函数才能返回到原点,即返回到原来 co resumed内的位置 在前文解释 st Co routine eny t结构 p CallStack这个“调用栈”的时候,我们已经简要 地提到了 yield摸作的內部逻辑。在被调协程要让出CPU时,会将它的 coroutine t从 cAllsTack弹出,“栈指针” cAllstack size减1,然后 co Swap()切换CPU上下文到原来 被挂起的调用者协程恢复执行。这旦“被拦起的调用者协程”,即是调用者 co resume( 中切换CPU上下文被挂起的那个协程。下面我们来看一下 co yield envo函数代码 10

...展开详情
所需积分/C币:15 上传时间:2019-09-17 资源大小:473KB
举报 举报 收藏 收藏
分享 分享
html+css+js制作的一个动态的新年贺卡

该代码是http://blog.csdn.net/qq_29656961/article/details/78155792博客里面的代码,代码里面有要用到的图片资源和音乐资源。

立即下载
Camtasia 9安装及破解方法绝对有效

附件中注册方法亲测有效,加以整理与大家共享。 由于附件大于60m传不上去,另附Camtasia 9百度云下载地址。免费自取 链接:http://pan.baidu.com/s/1kVABnhH 密码:xees

立即下载
电磁场与电磁波第四版谢处方 PDF

电磁场与电磁波第四版谢处方 (清晰版),做天线设计的可以作为参考。

立即下载
压缩包爆破解密工具(7z、rar、zip)

压缩包内包含三个工具,分别可以用来爆破解密7z压缩包、rar压缩包和zip压缩包。

立即下载
算法第四版 高清完整中文版PDF

《算法 第4版 》是Sedgewick之巨著 与高德纳TAOCP一脉相承 是算法领域经典的参考书 涵盖所有程序员必须掌握的50种算法 全面介绍了关于算法和数据结构的必备知识 并特别针对排序 搜索 图处理和字符串处理进行了论述 第4版具体给出了每位程序员应知应会的50个算法 提供了实际代码 而且这些Java代码实现采用了模块化的编程风格 读者可以方便地加以改造

立即下载
c语言课程设计 NBA球星管理系统

c语言课程设计代码实现,程序运行过程中采用链表对数据进行存储,实现了对链表的操作,程序运行结束用txt文件对信息进行存储,实现了数据的加密保存,采用系统函数及布局对界面进行美化,采用字符串对输入信息进行判断并保存,防止了错误的输入.并赋有实验报告

立即下载
rar.zip.7z密码破解

可以破解大部分压缩软件的密码。不用担心密码忘记。好用。

立即下载
《电路》邱关源-第五版.pdf

邱关源,出生于1923年(癸亥年),汉族,是西安交通大学教授,博士生导师,国内著名的电路理论专家,曾任国家教育部电工课程教学指导委员会委员。

立即下载
jdk1.8下载

jdk1.8下载

立即下载
DroidCamX 6.5 电脑端和手机端(2018年版本)

DroidCamX 6.5 适配安卓8.0和win10系统。让你的安卓手机变成摄像头。

立即下载