libeio用处

来源:互联网 发布:web服务器默认端口 编辑:程序博客网 时间:2024/05/16 07:51

顺手写了个程序对比了一下多线程IO和单线程异步IO的性能差异。需要说明的是,Linux上目前的异步IO是由用户态线程模拟的。目前内核原生的AIOglibc中的异步IO都有缺陷,libeio目前感觉比较好(因为nodejs在用)。单线程异步IO指的是主线程中控制IO的代码全部都是在主线程中执行的,libeio内部使用的线程对外来说完全不可感知。单线程异步IO的好处就是主流程中的控制代码完全处于单线程环境,可以完全不用考虑锁的问题。


应用场景客户端启动20个线程同时从服务端求数据。服务端从磁盘出文件回给客户端。服务端有两种实现:1、使用多线程每个线程服务一个用户。2、使用单线程和异步IO来服务多个用户。


测试时服务端的处理器为单核600MHZArmV6RaspberryPi),客户端为2.5GHz Intel Core i52013Macmini),因此客户端不会成为性瓶可以体现出服务端的差异。两台机器用100M网线直连。


20个线程下载700KB的文件:(测4次录每次成绩,数据为所花费的时间,越少越好)


多线程

183393

183505

183792

182878


异步IO

198165

191537

196565

193653


20个线程下载40K文件


多线程

12434

13099

12773

12958


异步IO

15293

15073

15974

15631


两种方案的优缺点对比

1、性上传统的多线程方案占有优势。不过测程序中多线程中完全没有使用锁,现实中由于的锁的限制多线程的性将大打折扣。

2、单线程异步IO程序中,由于控制代码在单线程环境中运行,可以完全无锁。

3、单线程异步IO程序中同时服务20个用户只使用了3个线程(libeio使用线程模拟异步IO,当前Linux上异步IO没有更好的决方案),多线程版本的程序使用了21个线程(包括主线程)。由于线程多,因此多线程版本的内存占用比libeio版本了很多。

4libieo版本的流程没有多线程版本的直观。


总结

单线程异步IO的主流程代码可以完全无锁,这个是单线程异步IO的最大优势。在性能损失不多的情况下(和完全无锁的多线程方案对比,现实中完全无锁的多线程环境几乎不存在),很值得考虑。唯一需要考虑的,单线程异步IO可能对多核CPU的利用不如传统的多线程充分。

另外,还可以扩展一下,在编写复杂应用时,除了IO等阻塞操作,CPU密集的操作也可以拿出来到单独的线程中执行,尽量把耗时不多的逻辑部分留在单线程中,由于运行在单线程环境,这样很大程度上简化逻辑部分的编写。



相信上面这段话已经将libeio的feature讲的足够清楚:提供全套异步文件操作的接口,让使用者能写出完全非阻塞的程序。阻塞意味着低效,但非阻塞一定要有很好的通知机制才能做到高效。

其实linux下的AIO(异步IO)并不是没有解决方案:在用户态,多线程同步来模拟的异步IO,如Glibc 的AIO;以及在内核态实现异步通知,如linux内核2.6.22之后实现的Kernel Native AIO。但两者都存在让使用者望而祛步的问题。

Glibc的AIO bug太多,而且IO发起者并不是最后的IO终结者(callback是在单独的线程执行的);而kernel Native AIO只支持O_DIRECT方式,无法利用Page cache。

正是由于上述原因,Marc Alexander Lehmann大佬决定自己开发一个AIO库,及libeio。libeio也是在用户态用多线程同步来模拟异步IO,但实现更高效,代码也更可靠,目前虽然是beta版,但已经可以上生产了(node.js底层就是用libev和libeio来驱动的)。还要强调一点:libeio里IO的终结者正是当初IO的发起者(这一点非常重要,因为IO都是由用户的request而发起,而IO完成后返回给用户的response也能在处理request的线程中完成)。

实现

一次异步IO操作可以分为三个阶段:

初始化         –>  提交IO        –>  通知worker线程      (主线程);

取request     –>  执行IO        –>  通知主线程               (worker线程);

取response  –>  callback        –>  结束IO                      (主线程)。

下面我们通过一张流程图来剖析一下libeio的源码实现。

1. 主线程调用eio_init函数,主要是初始化req_queue,res_queue以及对应的mutex和cond;

2-3. 所有的IO操作其实都是对eio_sumbit的调用,而eio_sumbit的职能是将IO操作封装为request并插入到req_queue;并调用cond_signal向worker线程发出reqwait已经OK的信号;

libeio处理流程图

4. worker线程被创建后执行的函数为etp_proc,etp_proc启动后会一直等待reqwait条件的出现;

5-6. 当reqwait条件变量满足时,etp_proc从req_queue中取得一个待处理的request;并调用eio_execute来同步执行该IO操作;

7-8. eio_execute完成后,将response插入到res_queue队列中;同时调用want_poll来通知主线程request已经处理完毕;

9. 这里worker线程通知主线程的机制是通过向pipe[1]写一个byte数据;

10. 当主线程发现pipe[0]可读时,就调用eio_poll;

11. eio_poll从res_queue里取response,并调用该IO操作在init时设置的callback函数完成后续处理;

12. 在res_queue中没有待处理response时,调用done_poll;

13-14. done_poll从pipe[0]读出一个byte数据,该IO操作完成。

后记

libeio的实现就是这么简洁,这里需要说明两点:

1. 在worker线程完成IO请求,通知主线程的机制是需要使用者自定义的,wait_poll和done_poll就是libeio提供给使用者的接口(pipe是一种常用的线程通知机制)。

2. worker线程并不是为每个请求都创建一个,而是维护了一个worker线程池,关于这部分将会在下面的文章中单独讲到。