您好,欢迎访问三七文档
当前位置:首页 > 商业/管理/HR > 经营企划 > C++开源协程库libco-原理与应用
C++开源协程库libco——原理及应用滴滴平台技术部·王亮2016年11月26日1导论使用C++来编写高性能的网络服务器程序,从来都不是件很容易的事情。在没有应用任何网络框架,从epoll/kqueue直接码起的时候尤其如此。即便使用libevent,libev这样事件驱动的网络框架去构建你的服务,程序结构依然不会很简单。为何会这样?因为这类框架提供的都是非阻塞式的、异步的编程接口,异步的编程方式,这需要思维方式的转变。为什么golang近几年能够大规模流行起来呢?因为简单。这方面最突出的一点便是它的网络编程API,完全同步阻塞式的接口。要并发?go出一个协程就好了。相信对于很多人来说,最开始接触这种编程方式,是有点困惑的。程序中到处都是同步阻塞式的调用,这程序性能能好吗?答案是,好,而且非常好。那么golang是如何做到的呢?秘诀就在它这个协程机制里。在go语言的API里,你找不到像epoll/kqueue之类的I/O多路复用(I/Omultiplexing)接口,那它是怎么做到轻松支持数万乃至十多万高并发的网络IO的呢?在Linux或其他类Unix系统里,支持I/O多路复用事件通知的系统调用(SystemCall)不外乎epoll/kqueue,它难道可以离开这些系统接口另起炉灶?这个自然是不可能的。聪明的读者,应该大致想到了这背后是怎么个原理了。语言内置的协程并发模式,同步阻塞式的IO接口,使得golang网络编程十分容易。那么C++可不可以做到这样呢?本文要介绍的开源协程库libco,就是这样神奇的一个开源库,让你的高性能网络服务器编程不再困难。Libco是微信后台大规模使用的C++协程库,在2013年的时候作为腾讯六大开源项目首次开源。据说2013年至今稳定运行在微信后台的数万台机器上。从本届ArchSummit北京峰会来自腾讯内部的分享经验来看,它在腾讯内部使用确实是比较广泛的。同go语言一样,libco也是提供了同步风格编程模式,同时还能保证系统的高并发能力。12准备知识2.1协程(Coroutine)是什么?协程这个概念,最近这几年可是相当地流行了。尤其go语言问世之后,内置的协程特性,完全屏蔽了操作系统线程的复杂细节;甚至使go开发者“只知有协程,不知有线程”了。当然C++,Java也不甘落后,如果你有关注过C++语言的最新动态,可能也会注意到近几年不断有人在给C++标准委员会提协程的支持方案;Java也同样有一些试验性的解决方案在提出来。在go语言大行其道的今天,没听说过协程这个词的程序员应该很少了,甚至直接接触过协程编程的(golang,lua,python等)也不在少数。你可能以为这是个比较新的东西,但其实协程这个概念在计算机领域已经相当地古老了。早在七十年代,DonaldKnuth在他的神作TheArtofComputerProgramming中将Coroutine的提出者归于ConwayMelvin。同时,Knuth还提到,coroutines不过是一种特殊的subroutines(Subroutine即过程调用,在很多高级语言中也叫函数,为了方便起见,下文我们将它称为“函数”)。当调用一个函数时,程序从函数的头部开始执行,当函数退出时,这个函数的声明周期也就结束了。一个函数在它的生命周期中,只可能返回一次。而协程则不同,协程在执行过程中,可以调用别的协程自己则中途退出执行,之后又从调用别的协程的地方恢复执行。这有点像操作系统的线程,执行过程中可能被挂起,让位于别的线程执行,稍后又从挂起的地方恢复执行。在这个过程中,协程与协程之间实际上不是普通“调用者与被调者”的关系,他们之间的关系是对称的(symmetric)。实际上,协程不一定都是这种对称的关系,还存在着一种非对称的协程模式(asymmetriccoroutines)。非对称协程其实也比较常见,本文要介绍的libco其实就是一种非对称协程,BoostC++库也提供了非对称协程。具体来讲,非对称协程(asymmetriccoroutines)是跟一个特定的调用者绑定的,协程让出CPU时,只能让回给原调用者。那到底是什么东西“不对称”呢?其实,非对称在于程序控制流转移到被调协程时使用的是call/resume操作,而当被调协程让出CPU时使用的却是return/yield操作。此外,协程间的地位也不对等,caller与callee关系是确定的,不可更改的,非对称协程只能返回最初调用它的协程。对称协程(symmetriccoroutines)则不一样,启动之后就跟启动之前的协程没有任何关系了。协程的切换操作,一般而言只有一个操作,yield,用于将程序控制流转移给另外的协程。对称协程机制一般需要一个调度器的支持,按一定调度算法去选择yield的目标协程。Go语言提供的协程,其实就是典型的对称协程。不但对称,goroutines还可以在多个线程上迁移。这种协程跟操作系统中的线程非常相似,甚至可以叫做“用户级线程”了。而libco提供的协程,虽然编程接口跟pthread有点类似,“类pthread的接口设计”,“如线程库一样轻松”,本质上却是一种非对称协程。这一点不要被表象蒙蔽了。事实上,libco内部还为保存协程的调用链留了一个stack结构,而这个stack大小只有固定的128。使用libco,如果不断地在一个协程运行过程中启动另一个协程,随着嵌套深度增加就可能会造成这个栈空间溢出。22.2栈的概念回顾TBD3Libco使用简介3.1一个简单的例子在多线程编程教程中,有一个经典的例子:生产者消费者问题。事实上,生产者消费者问题也是最适合协程的应用场景。那么我们就从这个简单的例子入手,来看一看使用libco编写的生产者消费者程序(例程代码来自于libco源码包)。1structstTask_t{2intid;3};45structstEnv_t{6stCoCond_t*cond;7queuestTask_t*task_queue;8};910void*Producer(void*args){11co_enable_hook_sys();12stEnv_t*env=(stEnv_t*)args;13intid=0;14while(true){15stTask_t*task=(stTask_t*)calloc(1,sizeof(stTask_t));16task id=id++;17env task_queue.push(task);18printf(%s:%dproducetask%d\n,__func__,__LINE__,task id);19co_cond_signal(env cond);20poll(NULL,0,1000);21}22returnNULL;23}2425void*Consumer(void*args){26co_enable_hook_sys();27stEnv_t*env=(stEnv_t*)args;28while(true){29if(env task_queue.empty()){30co_cond_timedwait(env cond, 1);31continue;32}33stTask_t*task=env task_queue.front();34env task_queue.pop();35printf(%s:%dconsumetask%d\n,__func__,__LINE__,task id);36free(task);37}38returnNULL;39}生产者和消费者协程3在上面的代码中,Producer与Consumer函数分别实现了生产者与消费者的逻辑,函数的原型跟pthread线程函数原型也是一样的。不同的是,在函数第一行还调用了一个co_enable_hook_sys(),此外,不是用sleep()去等待,而是poll()。这些原因后文会详细解释,暂且不管。接下来我们看怎样创建和启动生产者和消费者协程。1intmain(){2stEnv_t*env=newstEnv_t;3env cond=co_cond_alloc();45stCoRoutine_t*consumer_routine;6co_create(&consumer_routine,NULL,Consumer,env);7co_resume(consumer_routine);89stCoRoutine_t*producer_routine;10co_create(&producer_routine,NULL,Producer,env);11co_resume(producer_routine);1213co_eventloop(co_get_epoll_ct(),NULL,NULL);14return0;15}创建和启动生产者消费者协程初次接触libco的读者,应该下载源码编译,亲自运行一下这个例子看看输出结果是什么。实际上,这个例子的输出结果跟多线程实现方案是相似的,Producer与Consumer交替打印生产和消费信息。再来看代码,在main()函数中,我们看到代表一个协程的结构叫做stCoRoutine_t,创建一个协程使用co_create()函数。我们注意到,这里的co_create()的接口设计跟pthread的pthread_create()是非常相似的。跟pthread不太一样是,创建出一个协程之后,并没有立即启动起来;这里要启动协程,还需调用co_resume()函数。最后,pthread创建线程之后主线程往往会pthread_join()等等子线程退出,而这里的例子没有“co_join()”或类似的函数,而是调用了一个co_eventloop()函数,这些差异的原因我们后文会详细解析。然后再看Producer和Consumer的实现,细心的读者可能会发现,无论是Producer还是Consumer,它们在操作共享的队列时都没有加锁,没有互斥保护。那么这样做是否安全呢?其实是安全的。在运行这个程序时,我们用ps命令会看到这个它实际上只有一个线程。因此在任何时刻处理器上只会有一个协程在运行,所以不存在raceconditions,不需要任何互斥保护。还有一个问题。这个程序既然只有一个线程,那么Producer与Consumer这两个协程函数是怎样做到交替执行的呢?如果你熟悉pthread和操作系统多线程的原理,应该很快能发现程序里co_cond_signal()、poll()和co_cond_timedwait()这几个关键点。换作是一个pthread编写的生产者消费者程序,在只有单核CPU的机器上执行,结果是不是一样的?总之,这个例子跟pthread实现的生产者消费者程序是非常相似的。通过这个例子,我们也大致对libco的协程接口有了初步的了解。为了能看懂本文接下来的内容,建议把其他几个例子的代码也都浏览一下。下文我们将不再直接列出libco例子中的代码,如果有引用到,请自行参看相关代码。44libco的协程通过上一节的例子,我们已经对libco中的协程有了初步的印象。我们完全可以把它当做一种用户态线程来看待,接下来我们就从线程的角度来开始探究和理解它的实现机制。以Linux为例,在操作系统提供的线程机制中,一个线程一般具备下列要素:(1)有一段程序供其执行,这个是显然是必须的。另外,不同线程可以共用同一段程序。这个也是显然的,想想我们程序设计里经常用到的线程池、工作线程,不同的工作线程可能执行完全一样的代码。(2)有起码的“私有财产”,即线程专属的系统堆栈空间。(3)有“户口”,操作系统教科书里叫做“进(线)程控制块”,英文缩写叫PCB。在Linux内核里,则为task_struct的一个结构体。有了这个数据结构,线程才能成为内核调度的一个基本单位接受内核调度。这个结构也记录着线程占有的各项资源。此外,值得一提的是,操作系统的进程还有自己专属的内存空间(用户态内存空间
本文标题:C++开源协程库libco-原理与应用
链接地址:https://www.777doc.com/doc-5234853 .html