文章归档

ae的“陷阱”?

ae (redis:ae.h ae.c)  ->  Asynchronous Event (libevent)

redis之后,ae这东西都快要酱成糊了。似乎很多人都在用,antirez 当初没有直接引入libev或者libevent这样的外部库,而是从中抽取了两个文件(就是redis src目录下的 ae.h ae.c 文件)改成适合自己的东西。其实ae不复杂,就是对epoll的API进行了一层简单的封装。

今天在写一个server的时候忽然想到了这个问题,遗留下来的,可惜一直没怎么注意过去看ae的实现而且一直纠结的认为是epoll工作原理的问题以致在理解方面走了很多弯路,很长的一段时间里我一度视ae为epoll的代名词,实际上不是那么一回事。

因为ae(或者说epoll)并发处理的性能实在太好了,好到每当我使用ae的时候都在犹豫要不要在回调函数里给全局变量加锁的笑剧,ae并发处理得那么快,我担心会不同步。举个例子,我用ae监听listenfd描述符,如果有连接来,执行一个回调函数,如 cbfunc(),cbfunc 会accept()返回一个client的sockfd,然后进行某些数据处理,期间会占用到我上面提到的某个全局变量一段时间。但是如果cbfunc()在执行期间,listenfd有有了一个新事件,有新的客户端连接进来了,然后继续执行回调函数cbfunc(),这里注意,是两个cbfunc()在同时执行吗?还是第二个cbfunc()的执行会等待第一个执行完成?如果不是,需要锁吗?

为了形象一下这个过程,我使用了nessDB和redis-cli两个程序来做模拟,因为nessDB就是使用了这个ae,而且nessDB兼容部分redis协议。

我修改了nessDB的db-server.c代码,阻塞第一个client连接10s,看看第二个client是否会先执行还是一直等待第一个client完成任务。

  1. ......
  2. static int cli_tok = 0;
  3. ......
  4. if (cli_tok == 0) {
  5.         cli_tok = fd;
  6.         sleep(10);
  7. }
  8. ......

cli_tok是全局变量,默认初始化为0,cli_tok的作用是标志当前链接是否为第一个连接,因为第一个连上server的client会对cli_tok修改,而修改之后cli_tok的值非0,其他的连接就会跳过sleep(10)的过程。然后启动db-server进程,启动两个redis-cli实例A和B。A先连接上db-server,然后执行一条get命令,然后B再连接上db-server,同样执行一条get命令,结果:

  1. $ ./redis-cli    (A)
  2. redis 127.0.0.1:6379> get fangdong
  3. "fangdong_ok"
  4. (10.00s)
  5. redis 127.0.0.1:6379>
  1. $ ./redis-cli    (B)
  2. redis 127.0.0.1:6379> get fangdong
  3. "fangdong_ok"
  4. (7.17s)
  5. redis 127.0.0.1:6379>

显然,B任务赤裸裸的被A阻塞了。

再看看ae中主要的实现代码

  1. #include "ae.h"
  2. ......
  3. n = epoll_wait(el->efd, el->ready, AE_SETSIZE, tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
  4. n = n > 0 ? n : 0;
  5. for (i = 0; i < n; i++) {
  6.         ee = el->ready + i;
  7.         e = &el->events[ee->data.fd];
  8.         if (ee->events & EPOLLIN)
  9.                 mask = mask | AE_READABLE;
  10.         if (ee->events & EPOLLOUT)
  11.                 mask = mask | AE_WRITABLE;
  12.         if (e->mask & mask & AE_READABLE) {
  13.                 rf = 1;
  14.                 e->rcb(el,ee->data.fd,e->data,mask);
  15.         }
  16.         if (e->mask & mask & AE_WRITABLE)
  17.                 if (rf != 1 || e->rcb != e->wcb)
  18.                         e->wcb(el,ee->data.fd,e->data,mask);
  19. }
  20. ......

line:3和line:5的for循环,ae通过调用epoll_wait获得已经准备好了文件描述符表,然后轮询执行该事件注册了的回调函数e->wcb或者e->rcb。很好,问题就出现在这里了,通常在实现server的时候,我们会习惯将数据处理过程放在回调函数里,比如cbfunc(),如果cbfunc()执行比较耗时,那么期间整个进程就阻塞在这里,得不到好的并发性能。

如果要处理长连接的问题,就需要引入业务线程(池),在执行回调函数的时候,将任务打包挂载到业务线程上去处理从而让回调函数尽可能快的瞬间返回。

Leave a Reply

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>