协程与网络编程–coroutine_event

之前在协程实现的基础这篇文章说过了协程的概念,协程为克服同步模型和异步模型的缺点,并使结合两者的优点成为了可能。

这里要介绍的就是coroutine_event,它基于libevent,提供了基于协程的并发模型。

coroutine_event与一般的协程库不同,它并没有自己的调度管理器,因为我只想它让普通的异步程序开发变得更容易而已。那既然它没有调度管理器,一旦某个协程挂起了,它该怎么样才能再度恢复执行呢?答案就是此协程要等到某个它等待的读写事件来临时才会再度被恢复运行。

协程的运行大概是这样的过程:

A协程发起读请求,发现这时候并没有数据可读,这时候它主动放弃CPU。libevent继续它的事件循环。这时候假设A有读事件,libevent会调用相应的回调,回调里会将A协程恢复运行。

这样,对于协程来说,里面的处理都是同步的;但是对于libevent的事件循环来说,它却是异步的。这里其实类似于我们平常写的状态机,只不过这里的状态机就是整个协程的上下文,而且我们并不用关心它的挂起和恢复。

好了,废话说完了,那就上echo_server例子吧。

在创建协程前,需要先做一些准备工作。

上面的channel_create实际上是创建了一个chan,它提供了协程间通讯的通道,它本质上就类似于一个阻塞的队列,第一个参数表示传递元素的大小,第二参数表示可以容纳多少个元素;ev_base就是libevent事件循环的基础,一般来说一个线程一个event_base的实例(即一个线程有一个事件循环);coroutine_base_t就是协程运行的基础了,一个线程里面的所有协程共同拥有一个coroutine_base_t的实例,coroutine_base_t依赖于event_base,其实就是因为协程本身的调度依赖于libevent的事件循环。

这里我们创建了两个协程,一个协程用来accept新的连接,一个协程用来处理连接请求。

上面是处理accept新连接的函数,它处在一个不断的循环当中,coroutine_accept是对accept的封装,在没有新连接时自动挂起此协程,这对使用者是透明的;当有新的连接时,会将fd写入到chan里去。这里会发现并不是用chan做参数(chan_sendl),而是用了一个chan_peer_t的实例。这是因为当chan的空间满时写入数据协程会被挂起,但上面已经说了协程的调度是依赖于libevent的事件循环,所以chan_peer_t里面是放了一对管道fd,当有其他协程从chan里读数据时就随机挑一个挂起的协程发送一个读事件给它,让libevent的事件循环将它恢复运行;这里抽出来是为了避免每次读写chan时都需要创建新的管道。

这是新连接的处理函数,它不断地从chan里读出新的fd,然后为它创建一个协程,这里可以看出每个连接的真正处理函数是request_handler。

上面的函数不断地从新的连接里面读出数据,然后将数据回写回去,coroutine_read和coroutine_write就是read、write的封装,跟coroutine_accept差不多,主要是在没有数据或者缓冲区没有空间时将协程挂起。

通过上面的例子,我们采用了coroutine per connection的模型,和传统的thread per connection一样简洁清晰,但却拥有异步运行的效率,所有的协程都是在同一个线程里面运行的。也许你会奇怪为什么不直接在accept到新的连接时直接新建新的协程来处理请求,其实这样是可以的,只是这里为了顺带演示一下协程之间的通信方式。

协程的切换并不用经过内核态,而且只需要几十条机器指令,非常快速。上面简单的echo_server在一千的并发下效率并不会比直接使用libevent慢多少,甚至很接近。但由于协程本质上是一个运行上下文,需要有栈空间来保存局部变量,目前这里的实现还需要考虑栈的大小,避免空间浪费或者局部变量所占的空间超过栈大小而导致栈溢出。

在伸缩性方面,可以通过每个线程绑定一个event_base和一个coroutine_base_t,这样就可以利用多核来提高效率。但目前在做压力测试时发现chan在跨线程使用时有一点小问题,就是可能导致glibc缓存过多内存而不返回给操作系统,但并不是内存泄露,这点还得待解决。

标签: , , ,
文章分类 Unix/Linux, 网络编程
One comment on “协程与网络编程–coroutine_event
  1. Puck_Dot说道:

    看了一下coroutine_event代码的实现,学习中。

    [回复]

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*