简介
IO模型分类
服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种:
(1)同步阻塞IO(Blocking IO):即传统的IO模型。
(2)同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。
(3)异步阻塞IO(IO Multiplexing):即经典的Reactor设计模式,主要代表为IO多路复用。
(4)异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。
同步、异步,阻塞、非阻塞
同步和异步的概念描述的是用户线程与内核的交互方式,是从用户线程角度进行区分:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。
案例理解
如果你想打印文件:
同步阻塞:你到打印店排队打印,然后在那等着,直到打印完才能回去。
同步非阻塞:你把需要打印的文件交给店老板,然后就去购物。不过逛一会儿,就回打印店问有没有打印完成,若打印完成则取走文件。
异步阻塞:购物的时候,接到打印店电话,说打印完成了,让亲自去拿。
异步非阻塞:打印店打电话说,我们知道您的位置,一会给你送过来,安心购物就可以了。
同步阻塞IO
用户线程通过系统调用read发起IO读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。整个IO请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。JAVA传统的IO模型属于此种方式。
同步非阻塞IO
用户线程发起IO请求时立即返回,但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。即用户需要不断地调用read,尝试读取数据,直到读取成功后,才继续处理接收的数据。整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。SOCKET设置NON-BLOCK模式就是此种方式。
异步模型出现
在提出异步模型前,我们先看一个C10K问题。
随着互联网的普及,网站承受的并发请求越来越高,C10K就是指网站同时接受1W并发请求,那1W并发请求要求网站服务端需要同时开1W个线程来处理。
现在的CPU都是基于分时模型设计的,1核同时只能处理一个线程请求,而且切换线程对CPU和内存开销比较大。大概每次切换3微秒到8微秒间。
lmbench工具下载链接:https://sourceforge.net/projects/lmbench
如下使用lmbench3工具测试结果:
size=0k ovr=1.50
2 3.12
4 3.45
8 3.75
16 5.13
24 6.83
32 6.34
64 8.54
96 7.98
在1W请求的时候就是80毫秒,假如一个应用程序有20个接口高并发查询,就是1.6秒,这还仅仅是CPU切换时间,加上每次业务处理时间,那对用户体验来讲就很不友好了。
那如何解决线程切换开销问题,大方向有2种。一是加后端服务器来分摊请求,二是提高每个线程的处理与接收能力,第一种服务器开销大,小公司承受不起,第二种就是优化网络模型,提高处理能力,异步IO也因此出现。
异步阻塞IO
Reactor模型图
异步阻塞IO是以Reactor模型为基础进行设计构建的,Reactor模型也称为反应器模型,基础组成结构如下(参考http://www.dre.vanderbilt.edu/~schmidt/PDF/reactor-siemens.pdf):
图中各部分含义如下:
- Initiation Dispatcher :初始分派器,用来注册移除事件。
- Handles :表示操作系统管理的资源,我们可以理解为fd。
- Synchronous Event Demultiplexer :事件通知器,通知Handles资源变化。
- Event Handler :事件处理器的接口。
- Concrete Event Handler :事件处理器的实际实现。
各模块协作的步骤基本如下:
- 首先用户发起请求注册事件到Initiation Dispatcher中。
- Initiation Dispatcher调用每个Event Handler的get_handle接口获取其绑定的Handle。
- Initiation Dispatcher调用handle_events开始事件处理循环。在这里,Initiation Dispatcher会将步骤2获取的所有Handle都收集起来,使用Synchronous Event Demultiplexer来等待这些Handle的事件发生。
- 当某个Handle的事件发生时,Synchronous Event Demultiplexer通知Initiation Dispatcher。
- Initiation Dispatcher根据发生事件的Handle找出所对应的处理器进行处理。
IO多路复用
Reactor模型是基于系统内核的IO多路复用技术实现的,在redis、tomcat等组件中广泛使用。IO即为网络I/O,多路即为多个TCP连接,复用即为共用一个线程或者进程。该模型最大的优势是系统开销小,不必创建也不必维护过多的线程或进程,减少了上下文切换、资源竞争、CPU切换消耗以及各种锁操作。
IO多路复用常用的有3种模式,select、poll、epoll。
select模型
通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。
本文以redis5.0为例代码截取分析。
redis源码地址:http://download.redis.io/releases
linux2.6内核源码地址:http://ftp.sjtu.edu.cn/sites/ftp.kernel.org/pub/linux/kernel/v2.6
Redis的ae_select.c代码如下
#include <sys/select.h>
#include <string.h>
......
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, j, numevents = 0;
memcpy(&state->_rfds,&state->rfds,sizeof(fd_set));
memcpy(&state->_wfds,&state->wfds,sizeof(fd_set));
retval = select(eventLoop->maxfd+1,&state->_rfds,&state->_wfds,NULL,tvp);// 调用内核select函数
if (retval > 0) {
for (j = 0; j <= eventLoop->maxfd; j++) {
int mask = 0;
aeFileEvent *fe = &eventLoop->events[j];
if (fe->mask == AE_NONE) continue;
if (fe->mask & AE_READABLE && FD_ISSET(j,&state->_rfds))
mask |= AE_READABLE;
if (fe->mask & AE_WRITABLE && FD_ISSET(j,&state->_wfds))
mask |= AE_WRITABLE;
eventLoop->fired[numevents].fd = j;
eventLoop->fired[numevents].mask = mask;
numevents++;
}
}
return numevents;
}
select的接口定义如下:
extern int select (int __nfds,
fd_set *__restrict __readfds,fd_set *__restrict __writefds, fd_set *__restrict __exceptfds,
struct timeval *__restrict __timeout);
参数1表示待监听的集合里的最大文件描述符的值 + 1。
参数2、3 、4三个集合分别存放需要监听读、写、异常三个操作的文件描述符。
参数5表示超时时间。设为0则立刻扫描并返回,设为NULL则永远等待,直到有文件描述符就绪。
select.c内核源码:
int do_select(int n, fd_set_bits *fds, s64 *timeout)
{
struct poll_wqueues table;
poll_table *wait;
int retval, i;
rcu_read_lock();
retval = max_select_fd(n, fds);
rcu_read_unlock();
if (retval < 0)
return retval;
n = retval;
poll_initwait(&table);
wait = &table.pt;
if (!*timeout)
wait = NULL;
retval = 0;
for (;;) {// 轮询操作
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
long __timeout;
set_current_state(TASK_INTERRUPTIBLE);
inp = fds->in; outp = fds->out; exp = fds->ex;
rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
for (i = 0; i < n; ++rinp, ++routp, ++rexp) {// 遍历读、写、异常数组
unsigned long in, out, ex, all_bits, bit = 1, mask, j;
unsigned long res_in = 0, res_out = 0, res_ex = 0;
const struct file_operations *f_op = NULL;
struct file *file = NULL;
in = *inp++; out = *outp++; ex = *exp++;
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
int fput_needed;
if (file) {
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
if (f_op && f_op->poll)
mask = (*f_op->poll)(file, retval ? NULL : wait);// 调用文件对应poll操作
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit;retval++;
}
}
cond_resched();
}
if (res_in)
*rinp = res_in;
}
}
__set_current_state(TASK_RUNNING);
poll_freewait(&table);
return retval;
}
select 的开销大在于每次都要遍历扫描每一个文件描述符就绪状态,并且是从最小的描述符 0 开始比较,做了很多无用功,所以效率很低。随着文件描述符的增加,效率会越来越低。
epoll模型
epoll是之前的select的增强版本,要理解epoll,首先得了解epoll的三大关键要素:mmap、红黑树、rdllist链表。
- mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。内核可以直接看到epoll监听的句柄,效率高。
- epoll在实现上采用红黑树去存储所有套接字,当添加或者删除一个套接字时(epoll_ctl),都在红黑树上去处理,红黑树查询性能好,时间复杂度O(logN),也没有最大个数限制。
- epoll当把事件添加进来的时候时候会将事件与相应的程序建立回调关系,当相应的事件发生后,就会调用这个回调函数,该回调函数在内核中被称为:ep_poll_callback,这个回调函数其实就所把这个事件添加到rdllist这个双向链表中。一旦有事件发生,epoll就会将该事件添加到双向链表中。那么当我们轮询检查时只需要检查rdlist双向链表中是否有存在注册的事件,效率非常可观。
Redis的ae_epoll.c代码如下
#include <sys/epoll.h>
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));
if (!state) return -1;
state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
if (!state->events) {
zfree(state);
return -1;
}
state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
if (state->epfd == -1) {
zfree(state->events);
zfree(state);
return -1;
}
eventLoop->apidata = state;
return 0;
}
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0};
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;
ee.events = 0;
mask |= eventLoop->events[fd].mask; /* Merge old events */
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
return 0;
}
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;
retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
if (retval > 0) {
int j;
numevents = retval;
for (j = 0; j < numevents; j++) {
int mask = 0;
struct epoll_event *e = state->events+j;
if (e->events & EPOLLIN) mask |= AE_READABLE;
if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
if (e->events & EPOLLERR) mask |= AE_WRITABLE;
if (e->events & EPOLLHUP) mask |= AE_WRITABLE;
eventLoop->fired[j].fd = e->data.fd;
eventLoop->fired[j].mask = mask;
}
}
return numevents;
}
epoll的接口定义代码如下
/*
* 创建一个epoll的句柄,产生了一棵红黑树epitem在内核cache中
*/
int epoll_create(int size);
/*
* 可以理解为,增删改 fd 需要监听的事件
* epfd 是 epoll_create() 创建的句柄。
* op 表示 增删改
* epoll_event 表示需要监听的事件,Redis 只用到了可读,可写,错误,挂断 四个状态
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
* 可以理解为查询符合条件的事件
* epfd 是 epoll_create() 创建的句柄。
* epoll_event 用来存放从内核得到事件的集合
* maxevents 获取的最大事件数
* timeout 等待超时时间
*/
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
eventpoll.c实现代码如下
struct epitem {// 红黑树定义
/* RB-Tree node used to link this structure to the eventpoll rb-tree */
struct rb_node rbn;
/* List header used to link this structure to the eventpoll ready list */
struct list_head rdllink;
/* The file descriptor information this item refers to */
struct epoll_filefd ffd;
/* Number of active wait queue attached to poll operations */
int nwait;
/* List containing poll wait queues */
struct list_head pwqlist;
/* The "container" of this item */
struct eventpoll *ep;
/* The structure that describe the interested events and the source fd */
struct epoll_event event;
/*
* Used to keep track of the usage count of the structure. This avoids
* that the structure will desappear from underneath our processing.
*/
atomic_t usecnt;
/* List header used to link this item to the "struct file" items list */
struct list_head fllink;
/* List header used to link the item to the transfer list */
struct list_head txlink;
/*
* This is used during the collection/transfer of events to userspace
* to pin items empty events set.
*/
unsigned int revents;
};
/*
* It opens an eventpoll file descriptor by suggesting a storage of "size"
* file descriptors. The size parameter is just an hint about how to size
* data structures. It won't prevent the user to store more than "size"
* file descriptors inside the epoll interface. It is the kernel part of
* the userspace epoll_create(2).
*/
asmlinkage long sys_epoll_create(int size){
int error, fd;
struct eventpoll *ep;
struct inode *inode;
struct file *file;
DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_create(%d)\n",
current, size));
/*
* Sanity check on the size parameter, and create the internal data
* structure ( "struct eventpoll" ).
*/
error = -EINVAL;
if (size <= 0 || (error = ep_alloc(&ep)) != 0)
goto eexit_1;
/*
* Creates all the items needed to setup an eventpoll file. That is,
* a file structure, and inode and a free file descriptor.
*/
error = ep_getfd(&fd, &inode, &file, ep);
if (error)
goto eexit_2;
DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_create(%d) = %d\n",
current, size, fd));
return fd;
eexit_2:
ep_free(ep);
kfree(ep);
eexit_1:
DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_create(%d) = %d\n",
current, size, error));
return error;
}
asmlinkage long
sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event __user *event) {
int error;
struct file *file, *tfile;
struct eventpoll *ep;
struct epitem *epi;
struct epoll_event epds;
。。。。。。
/* Try to lookup the file inside our hash table */
epi = ep_find(ep, tfile, fd);
error = -EINVAL;
switch (op) {
case EPOLL_CTL_ADD:
if (!epi) {
epds.events |= POLLERR | POLLHUP;
error = ep_insert(ep, &epds, tfile, fd);// 插入数据到红黑树
} else
error = -EEXIST;
break;
case EPOLL_CTL_DEL:
if (epi)
error = ep_remove(ep, epi);
else
error = -ENOENT;
break;
case EPOLL_CTL_MOD:
if (epi) {
epds.events |= POLLERR | POLLHUP;
error = ep_modify(ep, epi, &epds);
} else
error = -ENOENT;
break;
}
/*
* The function ep_find() increments the usage count of the structure
* so, if this is not NULL, we need to release it.
*/
if (epi)
ep_release_epitem(epi);
up_write(&ep->sem);
eexit_3:
fput(tfile);
eexit_2:
fput(file);
eexit_1:
DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_ctl(%d, %d, %d, %p) = %d\n",
current, epfd, op, fd, event, error));
return error;
}
static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
struct file *tfile, int fd) {
int error, revents, pwake = 0;
unsigned long flags;
struct epitem *epi;
struct ep_pqueue epq;
error = -ENOMEM;
if (!(epi = kmem_cache_alloc(epi_cache, SLAB_KERNEL)))
goto eexit_1;
/* Item initialization follow here ... */
ep_rb_initnode(&epi->rbn);
INIT_LIST_HEAD(&epi->rdllink);
INIT_LIST_HEAD(&epi->fllink);
INIT_LIST_HEAD(&epi->txlink);
INIT_LIST_HEAD(&epi->pwqlist);
epi->ep = ep;
ep_set_ffd(&epi->ffd, tfile, fd);
epi->event = *event;
atomic_set(&epi->usecnt, 1);
epi->nwait = 0;
/* Initialize the poll table using the queue callback */
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);// 初始化队列
revents = tfile->f_op->poll(tfile, &epq.pt);
。。。。。。
return 0;
eexit_2:
ep_unregister_pollwait(ep, epi);
/*
* We need to do this because an event could have been arrived on some
* allocated wait queue.
*/
write_lock_irqsave(&ep->lock, flags);
if (ep_is_linked(&epi->rdllink))
ep_list_del(&epi->rdllink);
write_unlock_irqrestore(&ep->lock, flags);
kmem_cache_free(epi_cache, epi);
eexit_1:
return error;
}
/*
* This is the callback that is used to add our wait queue to the
* target file wakeup lists.
*/
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
poll_table *pt){
struct epitem *epi = ep_item_from_epqueue(pt);
struct eppoll_entry *pwq;
if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, SLAB_KERNEL))) {
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);// 调用回调函数
pwq->whead = whead;
pwq->base = epi;
add_wait_queue(whead, &pwq->wait);
list_add_tail(&pwq->llink, &epi->pwqlist);
epi->nwait++;
} else {
/* We have to signal that an error occurred */
epi->nwait = -1;
}
}
/*
* This is the callback that is passed to the wait queue wakeup
* machanism. It is called by the stored file descriptors when they
* have events to report.
*/
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
int pwake = 0;
unsigned long flags;
struct epitem *epi = ep_item_from_wait(wait);
struct eventpoll *ep = epi->ep;
DNPRINTK(3, (KERN_INFO "[%p] eventpoll: poll_callback(%p) epi=%p ep=%p\n",
current, epi->ffd.file, epi, ep));
write_lock_irqsave(&ep->lock, flags);
/*
* If the event mask does not contain any poll(2) event, we consider the
* descriptor to be disabled. This condition is likely the effect of the
* EPOLLONESHOT bit that disables the descriptor when an event is received,
* until the next EPOLL_CTL_MOD will be issued.
*/
if (!(epi->event.events & ~EP_PRIVATE_BITS))
goto is_disabled;
/* If this file is already in the ready list we exit soon */
if (ep_is_linked(&epi->rdllink))
goto is_linked;
list_add_tail(&epi->rdllink, &ep->rdllist);// 将该fd加入到epoll监听的就绪链表中
is_linked:
/*
* Wake up ( if active ) both the eventpoll wait list and the ->poll()
* wait list.
*/
if (waitqueue_active(&ep->wq))
__wake_up_locked(&ep->wq, TASK_UNINTERRUPTIBLE |
TASK_INTERRUPTIBLE);
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
asmlinkage long sys_epoll_wait(int epfd, struct epoll_event __user *events,
int maxevents, int timeout)
{
int error;
struct file *file;
struct eventpoll *ep;
。。。。。。
/* Time to fish for events ... */
error = ep_poll(ep, events, maxevents, timeout);// 判断是否有回调事件发生
eexit_2:
fput(file);
eexit_1:
DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_wait(%d, %p, %d, %d) = %d\n",
current, epfd, events, maxevents, timeout, error));
return error;
}
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
int maxevents, long timeout) {
int res, eavail;
unsigned long flags;
long jtimeout;
wait_queue_t wait;
retry:
write_lock_irqsave(&ep->lock, flags);
res = 0;
if (list_empty(&ep->rdllist)) {
init_waitqueue_entry(&wait, current);
__add_wait_queue(&ep->wq, &wait);
for (;;) {// 轮询操作
set_current_state(TASK_INTERRUPTIBLE);
if (!list_empty(&ep->rdllist) || !jtimeout)// 获取ep_poll_callback中存入的值
break;
write_unlock_irqrestore(&ep->lock, flags);
jtimeout = schedule_timeout(jtimeout);
write_lock_irqsave(&ep->lock, flags);
}
__remove_wait_queue(&ep->wq, &wait);
set_current_state(TASK_RUNNING);
}
eavail = !list_empty(&ep->rdllist);
write_unlock_irqrestore(&ep->lock, flags);
if (!res && eavail &&!(res = ep_events_transfer(ep, events, maxevents)) && jtimeout)
goto retry;
return res;
}
epoll操作fd的两种模式:
LT模式(水平触发):当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式(边缘触发):当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
LT与ET对比:
LT效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。
ET效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。
从本质上讲:与LT相比,ET模型是通过减少系统调用来达到提高并行效率的。
IO多路复用是最常使用的IO模型,但是其异步程度还不够“彻底”,因为它会阻塞线程。因此IO多路复用只能称为异步阻塞IO,而非真正的异步IO。
异步非阻塞IO
“真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。
Proactor模型
异步IO模型使用了Proactor设计模式实现了这一机制。
异步IO模型中,用户线程直接使用内核提供的异步IO API发起read请求,且发起后立即返回,继续执行用户线程代码。不过此时用户线程已经注册相应的事件处理器,然后操作系统开启独立的内核线程去处理IO操作。此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
当read请求的数据到达时,由内核负责读取数据,并写入用户指定的缓冲区中(异步IO都是操作系统负责将数据读写到应用传递进来的缓冲区中,操作系统扮演了重要角色)。这也是区别于Reactor的一点。
最后内核将read的数据和用户线程注册事件分发给内部Proactor,Proactor将IO完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数),完成异步IO。
实现技术
开源C++框架:ACE
开源C++开发框架 ACE 提供了大量平台独立的底层并发支持类(线程、互斥量等)。 同时在更高一层它也提供了独立的几组C++类,用于实现Reactor及Proactor模式。尽管它们都是平台独立的单元,但他们都提供了不同的接口。ACE Proactor在MS-Windows上无论是性能还在健壮性都更胜一筹,这主要是由于Windows提供了一系列高效的底层异步API。不幸的是,并不是所有操作系统都为底层异步提供健壮的支持。举例来说, 许多Unix系统就有麻烦。因此, ACE Reactor可能是Unix系统上更合适的解决方案。 正因为系统底层的支持力度不一,为了在各系统上有更好的性能,开发者不得不维护独立的好几份代码: 为Windows准备的ACE Proactor以及为Unix系列提供的ACE Reactor。真正的异步模式需要操作系统级别的支持。
C网络库:libevent
libevent是一个C语言写的网络库,官方主要支持的是类linux操作系统,最新的版本添加了对windows的IOCP的支持。
Boost.Asio类库
Boost.Asio是一个C++语言写的类库,其就是以Proactor这种设计模式来实现。
相比于IO多路复用模型,异步IO并不十分常用,不少高性能并发服务程序使用IO多路复用模型+多线程任务处理的架构基本可以满足需求。况且目前操作系统对异步IO的支持并非特别完善,更多的是采用IO多路复用模型模拟异步IO的方式(IO事件触发时不直接通知用户线程,而是将数据读写完毕后放到用户指定的缓冲区中)。
有错误之处或者理解不当请各位大佬指教。