JUST DO IT

我之所以在这里,只是因为我想要在这里

高效异步IO的设计开发

高效异步IO,按我理解起来应该不光要达到程序运行时的高效,当然也要达到开发效率的高效,其中还包括很重要一点就是质量,对于很多从未做过异步IO的人来说,初次尝试异步IO肯定会碰到不少困难,因为不光是对编程能力的考验,也需要开发者对操作系统的IO操作有相当的了解,包括他的机制和部分原理。异步IO中也有高效低效之分,但主要还是要看具体的应用到底需要什么样机制。比如大家熟知的select就是个非常通用且跨平台的方法,由于select中需要把大量的时间花在维护IO句柄上,导致其效率大打折扣,一般来说,对于小并发的异步IO操作,比如普通的客户端或者是小并发量的服务器,它的效率可能也足够了。关于select的效率问题其实从各平台上对于FD_SETSIZE的定义就能看出一些来,在windows平台上,FD_SETSIZE是64,在Linux平台上是1024,也就是说,对于平台提供商来说也不指望他们提供的select能给你多大的并发吞吐能力,但由于select的简单和普及,其应用面还是很广,很多时候确实也不需要太多的并发量。其实说到高效异步IO的开发,我们也说了,不光是考虑到程序运行时的效率,还要考虑开发的效率和软件的质量,说到这里,其实select这么个简单的机制有时候用起来也不那么简单,而且还会出很多错误。

说到select重复的维护句柄的开销,其实也是有解决方法的,好的解决方法效率会提高很多,但是重复工作还是要做的,比如当select返回结果是0,或者当能确定不需要增减IO句柄时,我们可以简单的把原先保存的FD_SET副本重新写入,这样可以减少重新生成FD_SET的开销,内存复制效率显然高于队列的一次次遍历,这是显而易见的。当然对于大并发量的IO操作来说,这种方法对于效率的提高也是很有限的,说到底即使采用异步IO效率也并不一定就高,还要取决于很多其它因素。其中很重要一点大家别忘记把侦听端口设置成异步的,虽然不设置的话程序运行后好像没有什么不正常,但光从表面上就可以看到CPU占用率明显偏高,当然还会有一些其它问题产生,这个问题比较复杂,这里不作描述了。并发操作的效率还取决于“连接-->断开->连接”的频度,频繁的“连接-->断开->连接”也会产生不小的开销,当然这些开销和之后要讲的真正实现业务操作需要的开销比起来其实要小得多,而且也是有很多方法可以规避的,对于采用不同的异步IO实现方法也是很不同的,效率差异会非常大。对我们来说,归根结底是根据不同的模型采用不同的方法来提高效率,作为一个异步IO的前端“发生器”应该尽可能避免在自己的工作上消耗过多的CPU资源,而是尽可能把CPU资源让给具体的业务实现者。

异步I/O中的Edge-Triggered和Level-Triggered是非常重要的概念;Edge-Triggered字面上理解就是指“边界触发”,说的是当状态变化的时候触发,以后如果状态一直没有变化或没有重新要求系统给出通知,将不再通知应用程序;Level-Triggered是指“状态触发”,说的是在某种状态下触发,如果一直在这种状态下就一直触发。两种触发方式各有用途,应根据不同的应用采用不同的触发方式。select一般默认采用的是Level-Triggered,而EPoll既可以采用Edge-Triggered,也可以采用Level-Triggered,默认是Level-Triggered,而MS的CPIO按这种定义来说应该属于Edge-Triggered。对于已经封装好的异步I/O架构来说,具体采用哪种方式其实无伤大雅,因为无论采用哪种方式,都需要在内部都实现正确了,并且让使用者不再关心这种具体的触发方式为好。
void EPollReactor::NotifyMeWrite(SOCKET handle, SvcHandler *handler)
{
    DIAMON_ASSERT(handle != INVALID_SOCKET);
    EPoll_Mod_Handle_Events(handle, EPOLLOUT/* | EPOLLET*/);
}
上述代码中,就是在每次写完数据后需要异步I/O框架再通知应用程序关于写完,其中EPoll_Mod_Handle_Events函数告诉系统给handle注册上EPOLLOUT消息,这样当handle完成写操作后,系统将通知框架写完消息,是否加上EPOLLET完全取决于框架同应用者之间的协议,其实本质上就是框架对外提供的接口和调用约定。在Diamon::ACE中,采用的是Level-Triggered方式。
void IOCPReactor::NotifyMeWrite(SOCKET handle, SvcHandler *handler)
{
    DIAMON_ASSERT(handle != INVALID_SOCKET);
    IOCPSvcHandler *iocphandler = (IOCPSvcHandler *)handler;
    iocphandler->event_ &= (~IOCP_EVENT_READ);
    iocphandler->event_ |= IOCP_EVENT_WRITE;
}
对比一下,CPIO中的NotifyMeWrite做的不是通知系统,而是告诉异步I/O框架自己接下来应该处理写事件了,而对系统的触发工作完全是交给WSAWrite...之类的函数来完成的。想想啊,每次调用WSAWrite...不正是对系统说,我给这个handle注册一下写事件啊,下次还通知我,你可以试试不调用WSAWrite...了,下次肯定收不到写通知了。

同步问题是高效异步IO设计中非常重要但又常被忽视的问题(更不能滥用),不好的同步方法有时会限制应用层的使用。我曾经犯过这么一个错误:select操作和FD_SET的操作是必须顺序进行的,否则会产生不可预期的后果。我们知道,在写数据操作后往往需要将写通知告诉应用层,因此需要在写操作后往写FD_SET中设置handle,但是写方法的调用和select方法的调用可能是在2个线程中,当我一开始遇到这个问题的时候,我只简单的在select的外围加了一对mutex操作,当应用层在收到读通知中直接调用写方法没有问题,因为这时写方法的调用和select调用处在一个线程中(肯定是顺序执行的),表面上看这个问题确实是解决了,但实际上却隐藏了一些很严重的问题,当写方法是在其他的“业务处理器”中,比如另一个线程中,那么这种调用就会导致FD_SET的操作和select操作有同时进行的可能,而当这种情况发生后,显而易见程序是不可能正常运行了。而对于应用程序来说,唯一解决这个问题的方法,就是要知道在select的外围的mutex对象,然后在自己调用写方法的外围再包上一对这个mutex操作,虽然解决了FD_SET操作和select操作同步的问题,但实际上却把问题更复杂化了,比如:一、让应用程序知道本不需要,更不应该让它知道的逻辑,导致了应用层开发的复杂性,甚至给应用程序带来由于错误使用导致更严重的问题;二、始终会存在某几种情况会导致互锁问题的产生。第二个问题可能会有点复杂,为了便于理解,我给大家解释一下互锁,我们考虑这么一种情况,有两个任务分别使用mutexA和mutexB,任务1中使用的顺序是...,mutexA.Lock(),...mutexB(),...,任务2中使用的顺序是...,mutexB.Lock(),...mutexA(),...,当任务1占用mutexA后等待mutexB前,任务2也刚好占用了mutexB在等待mutexA,这时候很明显就制造了一个互锁(交叉锁)。第二个问题其实就是由这种调用下导致的某种互锁情况。总之,我的这种不动脑子的解决问题的方法导致了严重的应用层问题。

转自: http://hippoweilin.mobile.spaces.live.com/arc.aspx

posted on 2009-07-25 11:18 xmoss 阅读(1525) 评论(0)  编辑 收藏 引用 所属分类: 网络相关


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理