没有合适的资源?快使用搜索试试~ 我知道了~
首页C++开源协程库libco-原理与应用
使用 C++ 来编写高性能的网络服务器程序,从来都不是件很容易的事情。在没有 应用任何网络框架,从 epoll/kqueue 直接码起的时候尤其如此。即便使用 libevent, libev 这样事件驱动的网络框架去构建你的服务,程序结构依然不会很简单。为何会这样?因 为这类框架提供的都是非阻塞式的、异步的编程接口,异步的编程方式,这需要思维方 式的转变。为什么 golang 近几年能够大规模流行起来呢?因为简单。这方面最突出的 一点便是它的网络编程 API,完全同步阻塞式的接口。要并发?go 出一个协程就好了。 相信对于很多人来说,最开始接触这种编程方式,是有点困惑的。程序中到处都是同步 阻塞式的调用,这程序性能能好吗?答案是,好,而且非常好。那么 golang 是如何做 到的呢?秘诀就在它这个协程机制里
资源详情
资源评论
资源推荐

C++ 开源协程库 libco——原理及应用
平台技术部·王亮 2016 年 11 月 26 日
1 导论
使用 C++ 来编写高性能的网络服务器程序,从来都不是件很容易的事情。在没有
应用任何网络框架,从 epoll/kqueue 直接码起的时候尤其如此。即便使用 libevent, libev
这样事件驱动的网络框架去构建你的服务,程序结构依然不会很简单。为何会这样?因
为这类框架提供的都是非阻塞式的、异步的编程接口,异步的编程方式,这需要思维方
式的转变。为什么 golang 近几年能够大规模流行起来呢?因为简单。这方面最突出的
一点便是它的网络编程 API,完全同步阻塞式的接口。要并发?go 出一个协程就好了。
相信对于很多人来说,最开始接触这种编程方式,是有点困惑的。程序中到处都是同步
阻塞式的调用,这程序性能能好吗?答案是,好,而且非常好。那么 golang 是如何做
到的呢?秘诀就在它这个协程机制里。
在 go 语言的 API 里,你找不到像 epoll/kqueue 之类的 I/O 多路复用(I/O multiplexing)
接口,那它是怎么做到轻松支持数万乃至十多万高并发的网络 IO 的呢?在 Linux 或
其他类 Unix 系统里,支持 I/O 多路复用事件通知的系统调用(System Call)不外乎
epoll/kqueue,它难道可以离开这些系统接口另起炉灶?这个自然是不可能的。聪明的
读者,应该大致想到了这背后是怎么个原理了。
语言内置的协程并发模式,同步阻塞式的 IO 接口,使得 golang 网络编程十分容
易。那么 C++ 可不可以做到这样呢?
本文要介绍的开源协程库 libco,就是这样神奇的一个开源库,让你的高性能网络
服务器编程不再困难。
Libco 是微信后台大规模使用的 C++ 协程库,在 2013 年的时候作为腾讯六大开
源项目首次开源。据说 2013 年至今稳定运行在微信后台的数万台机器上。从本届
ArchSummit 北京峰会来自腾讯内部的分享经验来看,它在腾讯内部使用确实是比较广
泛的。同 go 语言一样,libco 也是提供了同步风格编程模式,同时还能保证系统的高并
发能力。
1

2 准备知识
2.1
协程(
Coroutine
)是什么?
协程这个概念,最近这几年可是相当地流行了。尤其 go 语言问世之后,内置的协
程特性,完全屏蔽了操作系统线程的复杂细节;甚至使 go 开发者“只知有协程,不知
有线程”了。当然 C++, Java 也不甘落后,如果你有关注过 C++ 语言的最新动态,可能
也会注意到近几年不断有人在给 C++ 标准委员会提协程的支持方案;Java 也同样有一
些试验性的解决方案在提出来。
在 go 语言大行其道的今天,没听说过协程这个词的程序员应该很少了,甚至直接接
触过协程编程的(golang, lua, python 等)也不在少数。你可能以为这是个比较新的东西,
但其实协程这个概念在计算机领域已经相当地古老了。早在七十年代,Donald Knuth 在
他的神作 The Art of Computer Programming 中将 Coroutine 的提出者归于 Conway Melvin。
同时,Knuth 还提到,coroutines 不过是一种特殊的 subroutines(Subroutine 即过程调用,
在很多高级语言中也叫函数,为了方便起见,下文我们将它称为“函数”)。当调用一
个函数时,程序从函数的头部开始执行,当函数退出时,这个函数的声明周期也就结
束了。一个函数在它的生命周期中,只可能返回一次。而协程则不同,协程在执行过程
中,可以调用别的协程自己则中途退出执行,之后又从调用别的协程的地方恢复执行。
这有点像操作系统的线程,执行过程中可能被挂起,让位于别的线程执行,稍后又从
挂起的地方恢复执行。在这个过程中,协程与协程之间实际上不是普通“调用者与被调
者”的关系,他们之间的关系是对称的(symmetric)。实际上,协程不一定都是这种对
称的关系,还存在着一种非对称的协程模式(asymmetric coroutines)。非对称协程其实
也比较常见,本文要介绍的 libco 其实就是一种非对称协程,Boost C++ 库也提供了非
对称协程。
具体来讲,非对称协程(asymmetric coroutines)是跟一个特定的调用者绑定的,协
程让出 CPU 时,只能让回给原调用者。那到底是什么东西“不对称”呢?其实,非对称
在于程序控制流转移到被调协程时使用的是 call/resume 操作,而当被调协程让出 CPU
时使用的却是 return/yield 操作。此外,协程间的地位也不对等,caller 与 callee 关系是
确定的,不可更改的,非对称协程只能返回最初调用它的协程。
对称协程(symmetric coroutines)则不一样,启动之后就跟启动之前的协程没有任
何关系了。协程的切换操作,一般而言只有一个操作,yield,用于将程序控制流转移给
另外的协程。对称协程机制一般需要一个调度器的支持,按一定调度算法去选择
yield
的目标协程。
Go 语言提供的协程,其实就是典型的对称协程。不但对称,goroutines 还可以在
多个线程上迁移。这种协程跟操作系统中的线程非常相似,甚至可以叫做“用户级线
程”了。而 libco 提供的协程,虽然编程接口跟 pthread 有点类似,“类 pthread 的接口设
计”,“如线程库一样轻松”,本质上却是一种非对称协程。这一点不要被表象蒙蔽了。
事实上,libco 内部还为保存协程的调用链留了一个 stack 结构,而这个 stack 大小只有
固定的 128。使用 libco,如果不断地在一个协程运行过程中启动另一个协程,随着嵌套
深度增加就可能会造成这个栈空间溢出。
2

2.2 栈的概念回顾
3 Libco 使用简介
3.1 一个简单的例子
在多线程编程教程中,有一个经典的例子:生产者消费者问题。事实上,生产者消
费者问题也是最适合协程的应用场景。那么我们就从这个简单的例子入手,来看一看
使用 libco 编写的生产者消费者程序(例程代码来自于 libco 源码包) 。
1 struct stTask_t {
2 int id;
3 };
4
5 struct stEnv_t {
6 stCoCond_t* cond;
7 queue<stTask_t*> task_queue;
8 };
9
10 void* Producer(void* args) {
1 1 co_enable_hook_sys();
12 stEnv_t* env = (stEnv_t*)args;
13 int id = 0;
14 while (true) {
15 stTask_t* task = (stTask_t*)calloc(1, sizeof(stTask_t));
16 task− > i d = id++;
17 env− >task_queue.push(task);
18 printf("%s:%d produce task %d\n", __func__, __LINE__, task− >id);
19 co_cond_signal(env− >cond);
20 poll(NULL, 0, 1000);
21 }
22 return NULL;
23 }
24
25 void* Consumer(void* args) {
26 co_enable_hook_sys();
27 stEnv_t* env = (stEnv_t*)args;
28 while (true) {
29 if (env− >task_queue.empty()) {
30 co_cond_timedwait(env− >cond, − 1 ) ;
31 continue;
32 }
33 stTask_t* task = env− >task_queue.front();
34 env− >task_queue.pop();
35 printf("%s:%d consume task %d\n", __func__, __LINE__, task− >id);
36 free(task);
37 }
38 return NULL;
39 }
生产者和消费者协程
3
该文档是极速PDF编辑器生成,
如果想去掉该提示,请访问并下载:
http://www.jisupdfeditor.com/

在上面的代码中,Producer 与 Consumer 函数分别实现了生产者与消费者的逻辑,
函数的原型跟 pthread 线程函数原型也是一样的。不同的是,在函数第一行还调用了一
个 co_enable_hook_sys(),此外,不是用 sleep() 去等待,而是 poll()。这些原因后文会详
细解释,暂且不管。接下来我们看怎样创建和启动生产者和消费者协程。
1 int main() {
2 stEnv_t* env = new stEnv_t;
3 env−>cond = co_cond_alloc();
4
5 stCoRoutine_t* consumer_routine;
6 co_create(&consumer_routine, NULL, Consumer, env);
7 co_resume(consumer_routine);
8
9 stCoRoutine_t* producer_routine;
10 co_create(&producer_routine, NULL, Producer, env);
11 co_resume(producer_routine);
12
13 co_eventloop(co_get_epoll_ct(), NULL, NULL);
14 return 0;
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 命令会看到这个它实际上只有一
个线程。因此在任何时刻处理器上只会有一个协程在运行,所以不存在 race conditions,
不需要任何互斥保护。
还有一个问题。这个程序既然只有一个线程,那么 Producer 与 Consumer 这两个协
程函数是怎样做到交替执行的呢?如果你熟悉 pthread 和操作系统多线程的原理,应该
很快能发现程序里 co_cond_signal()、poll() 和 co_cond_timedwait() 这几个关键点。换作
是一个 pthread 编写的生产者消费者程序,在只有单核 CPU 的机器上执行,结果是不是
一样的?
总之,这个例子跟 pthread 实现的生产者消费者程序是非常相似的。通过这个例子,
我们也大致对 libco 的协程接口有了初步的了解。为了能看懂本文接下来的内容,建议
把其他几个例子的代码也都浏览一下。下文我们将不再直接列出 libco 例子中的代码,
如果有引用到,请自行参看相关代码。
4

4 libco 的协程
通过上一节的例子,我们已经对 libco 中的协程有了初步的印象。我们完全可以把
它当做一种用户态线程来看待,接下来我们就从线程的角度来开始探究和理解它的实
现机制。
以 Linux 为例,在操作系统提供的线程机制中,一个线程一般具备下列要素:
(1) 有一段程序供其执行,这个是显然是必须的。另外,不同线程可以共用同一段
程序。这个也是显然的,想想我们程序设计里经常用到的线程池、工作线程,不同的工
作线程可能执行完全一样的代码。
(2) 有起码的“私有财产”,即线程专属的系统堆栈空间。
(3) 有“户口”,操作系统教科书里叫做“进(线)程控制块”,英文缩写叫 PCB。在
Linux 内核里,则为 task_struct 的一个结构体。有了这个数据结构,线程才能成为内核
调度的一个基本单位接受内核调度。这个结构也记录着线程占有的各项资源。
此外,值得一提的是,操作系统的进程还有自己专属的内存空间(用户态内存空
间),不同进程间的内存空间是相互独立,互不干扰的。而同属一个进程的各线程,则
是共享内存空间的。显然,协程也是共享内存空间的。
我们可以借鉴操作系统线程的实现思想,在 OS 之上实现用户级线程(协程)。跟
OS 线程一样,用户级线程也应该具备这三个要素。所不同的只是第二点,用户级线程
(协程)没有自己专属的堆空间,只有栈空间。首先,我们得准备一段程序供协程执行,
这即是 co_create() 函数在创建协程的时候传入的第三个参数——形参为 void*,返回值
为 void 的一个函数。
其次,需要为创建的协程准备一段栈内存空间。栈内存用于保存调用函数过程中
的临时变量,以及函数调用链(栈帧)。在 Intel 的 x86 以及 x64 体系结构中,栈顶由
ESP(RSP)寄存器确定。所以一个创建一个协程,启动的时候还要将 ESP(RSP)切到
分配的栈内存上,后文将对此做详细分析。
co_create() 调用成功后,将返回一个 stCoRoutine_t 的结构指针(第一个参数)。从
命名上也可以看出来,该结构即代表了 libco 的协程,记录着一个协程拥有的各种资源,
我们不妨称之为“协程控制块”。这样,构成一个协程三要素——执行的函数,栈内存,
协程控制块,在 co_create() 调用完成后便都准备就绪了。
5 关键数据结构及其关系
1 struct stCoRoutine_t {
2 stCoRoutineEnv_t *env;
3 pfn_co_routine_t pfn;
4 void *arg;
5 coctx_t ctx;
6
7 char cStart;
8 char cEnd;
9 char cIsMain;
10 char cEnableSysHook;
11 char cIsShareStack;
5
剩余20页未读,继续阅读













安全验证
文档复制为VIP权益,开通VIP直接复制

评论4