ACE的陷阱
坦白说,使用这个标题无非是希望能够吸引你的眼球,这篇文章的目的仅仅是为了揭示一些ACE缺陷的。文章适合的读者是对ACE(ADAPTIVE Communication Environment)有一定研究,或者正在使用ACE从事项目开发的人士参考。如果你对C++还是新手,甚至包括ACE知识初学者,(但你想飞的更高),建议你收藏这篇文档以后阅读。
秉承陷阱系列文章的传统,我只是通过一些辩证的角度去看ACE的一些不足,对于ACE的强大和优美我就不再作赞美。从2000年,到现在,ACE在中国已经从星星之火,开始有燎原之势。这一方面说明ACE的优美和实力已经逐步得到大家的认可(我所知道的Adobe reader的使用ACE,估计是为了跨平台,国内的大量电信的网管,计费,智能网软件也使用ACE),一方面要感谢的是的马维达这位国内少有的职业作家,国内的ACE的中文资料(包括大量免费资料)都出自这位老兄。
但ACE无疑是复杂的,能够畅快的遨游在其中的绝对不是泛泛之辈。没有对网络,设计模式,操作系统有一定的底蕴,想痛快的驾驭ACE无疑是较难的。另外,由于ACE仍然处在逐步发展的过程中。他的很多问题仍然有待进一步完善。重要的是一些文案的不足,受众面狭小,导致许多ACE的使用者在使用ACE的时候会碰上很多问题。这篇文案就是用于彻底揭示部分这些问题。希望大家能在更加顺捷的使用它。
另外,请注意我使用的陷阱这个术语,而不是原罪。(C Trap and Pitfalls 倒有很多应该是Original sin)ACE还在不停的发展中。很多问题可能会在以后的版本中间改进。所以在我认为的的确是问题的章节后面,我会附上知道错误的版本号。
1 我将什么列为陷阱
1.1 低效的模块作为一个代码级的中间件。ACE无疑是高效的,但是坦白说ACE的代码不是非常完美的。ACE的很多地方提供的是一个框架解决方案,为了保证框架的可移植和通用,代码中大量使用了virtual 函数,Bridge模式,多线程下的锁操作,甚至有相当的new操作……,这些东西都限制ACE的性能。所以个人谨慎的将ACE的效率定义为中上。
个人认为,一般情况下,如果你使用ACE的API代替系统API,速度应该降低0.01%以下,主要导致这些差役在于ACE的再次封装,而函数栈的调用成本应该可以几乎不计。ACE的优势在高性能的系统架构,而不是绝对的函数性能,如果你要再考虑在加入系统框架的其它功能呢,(举一个例子,当你想把定时器优美的合入你的代码时),ACE就有足够的优势让你选择他。【注】
在此啰嗦一句,同样也有很多人质疑STL的性能。所有好的类库一样,他带来优势的同时也会有一定的遗憾,比如少量性能降低。但是如果说他们的性能不好,那是无稽之谈。(不信,把你认为性能差的代码给我写写看。)建议固步自封的程序员不要再干买椟还珠的事情,先去读读那些优美的代码。
但是和所有的框架一样,ACE也有不少的地方的地方是性能的暗礁,你最好绕开。当然一般而言ACE会提供多条道路,重要的是你能选择正确。
1.2 设计缺陷ACE的有多个层次,侧记缺陷这类错误往往出现在ACE的高阶封装中。同时由于ACE是一个跨平台的中间件。所以为了平台的兼容性,ACE做了很多折中和弥补,有些是很漂亮的,但有些却不是非常理想。
1.3 使用不便的地方所有的代码都是不完美的,特别是ACE这种要让无数人在无数环境下使用的软件。很多使用不便的问题都是来自我个人的一些习惯,这些算是苛责了。
1.4 容易误解或者误用的地方由于ACE的庞大性,很多时候大家会错误的理解使用ACE的某些代码实现某些特性。在此将写一些曾经让我们栽跟头的阴沟写出来。另一方面,ACE的文档的某些介绍也存在含混,会误导大家的理解,错误的地方。
2 ACE的链接Link错误很多人在Windows使用ACE的时候往往会出现以下的Link错误。
- Why do I get errors while using 'TryEnterCriticalSection'?/ace/OS.i(2384) : error C2039:'TryEnterCriticalSection': is not a member of '`global namespace''
复制代码其实这个错误不是由于ACE导致的,只是编译器把这个赃栽倒了ACE上。出现这个错误的原因主要是因为一些关键宏定义冲突,一般是_WIN32_WINNT,'TryEnterCriticalSection' 这个函数是NT4.0后才出现的函数,如果这个宏被定义的小于0x0400或者没有定义,那么就会出现这个错误。
所以最简单的处理方法是在自己的预定义头文件中加入一行。
- #if !defined (_WIN32_WINNT)# define _WIN32_WINNT 0x0400#endif
复制代码
其实ACE自己对于宏的处理是比较严谨的,ACE的config-win32-common.h中间就有这行定义,所以在一般而言,可以将ACE的头文件包含定义放在在顶部,这样也可以避免这个编译错误。
预定义头文件是一个良好的编程习惯,你可以将自己的大部分宏定义,include包含的本工程以外的外部.h文件。简言之就是预定义头文件中使用#include<>,表示包含工程以外文件,自己工程内部只使用#include””,表示包含当前工程目录下的文件。大部分C/C++的程序员都有过链接和一些预定义冲突错误消耗大量的时间,原来我也是如此,但是在掌握预定义头文件方法后,我几乎没有为这个问题折磨过。其实Virsual C++ 在生产MFC工程的时候,会自动帮你自动生产一个预定义头文件stdafx.h,只是我们不善利用而已。
其实对于很多编译器,使用预定义头文件还可以加快编译速度。Virusal C++的预定义会生产一个pch文件,基本可以提高编译速度一倍。Virusal C++的工程中间有专门的预定义头文件设置。C++ Builder采用可以采用的编译宏(好像是专用的)加快编译速度。大致的原理是编译器会在对预定义头文件中包含的文件进行与处理,在外部文件没有发生改动的时候,编译器可以使用编译这些文件生成的中间文件加快编译速度。
3 不要使用ACE_Timer_HashACE有一个非常优美的定时器队列模型,他提供了4种定时器Queue让大家使用:ACE_Timer_Heap,ACE_Timer_Wheel,ACE_High_Res_Timer,ACE_Timer_Hash。在《C++ Network Programming Volume 2 - Systematic Reuse with ACE and Frameworks》中间有相应的说明,其中按照说明最诱人的的是:
ACE_Timer_Hash, which uses a hash table to manage the queue. Like the timing wheel implementation, the average-case time required to schedule, cancel, and expire timers is O(1) and its worst-case is O(n).
但是遗憾的是,ACE_Timer_Hash其实是性能最差的。几乎不值得使用。我曾经也被诱惑过,但是在测试中间发现,文档中所述根本不属实,在一个大规模定时器的程序中,我使用ACE_Timer_Hash发现性能非常不理想,检查后发现ACE的源代码如下:
- template <class TYPE, class FUNCTOR, class ACE_LOCK, class BUCKET> intACE_Timer_Hash_T<TYPE, FUNCTOR, ACE_LOCK, BUCKET>::expire (const ACE_Time_Value &cur_time){ // table_size_为Hash的桶尺寸,如果要避免冲突,桶的数量应该尽量大,//每个桶可以理解为一个Hash开链的链表 // Go through the table and expire anything that can be expired //遍历所有的桶 for (size_t i = 0; i < this->table_size_; ++i) { //在每个桶中检查是否有要进行超时处理的元素 while (!this->table_->is_empty () && this->table_->earliest_time () <= cur_time) { …………
复制代码简单说明一下上面的代码,ACE_Timer_Hash_T采用开链的Hash方式,每个桶就是一个链表,在超时检查时所有的桶中是由有要进行超时处理的元素。所以在超时处理中ACE采用了遍历所有元素的方法。但悖论是如果你希望Hash的冲突不大,你就必须将桶的个数调整的尽量多。我在测试中将上述的程序的Time_Queue替换为标准的的ACE_Timer_Heap,发现性能提高数百倍。
冷静下来思考一下,这也是正常的。对于一个Hash的实现,保证查询的速度,也就是通过定时器ID进行操作的速度是足够快的。但是实际上对于定时器操作,最大的成本应该是寻找要超时的定时器,对于Hash这种数据结构,只能采用迭代遍历的方式……, 所以采用Hash的低效是正常的。而原文应该改为schedule, cancel,的最好时间复杂度是O(1),最差是O(n),而expire的时间复杂度始终是O(n)。
这个问题至少倒5.6.1的版本还是存在的。我个人估计也不会得到解决。Hash的特性摆在那儿呢,除非ACE采用更加复杂的数据结构。
4 Reactor定时器的精度取决于实现由于Reactor在各个平台的默认实现都取决于平台的实现,比如在Windows下默认的Reactor是WFMO_REACTOR,而在Linux和UNIX平台,默认的Reactor是Select_Reactor,而Reactor的实现往往取决于使用的反应器底层实现,而这些反应器的时间精度就决定了你的定时器的时间精度。下表大致反馈了一些常用的定时器的实现。
表1 常用Raactor的实现
Reactor
|
反应器的底层实现
|
时间精度
|
ACE_Select_Reactor
|
select函数
|
使用struct timeval结构进行超时处理; timeval 结构可以精确倒微秒。
|
Dev_Poll_Reactor
|
poll或者而epoll
|
timeout参数的单位是毫秒。
|
ACE_WFMO_REACTOR
|
WaitForMultipleObjects
|
dwMilliseconds 的参数单位是毫秒
|
|
|
|
不过作为服务器的开发,我倒想不出什么地方需要精确到0.1s定时器的地方,了解一下差异性就足够了。
5 WFMO_Reactor的与众不同WFMO_Reactor是ACE_Reactor在Windows下的默认实现(为什么不选择ACE_Select_Reactor作为默认实现,可能是基于效率和强大性的考虑),WFMO_Reactor的低层使用的函数是WaitForMultipleObjects和WSAEventSelect,WSAEnumNetworkEvents。其中WaitForMultipleObjects函数用于处理线程,互斥量,信号灯,事件,定时器等事件,而WSAEventSelect用于处理网络IO事件。
由于Windows API和操作系统的特性不一样,WFMO_Reactor在很多地方的表现和其他平台不一致。 【注】
【注】其实这两个问题在《C++ Network Programming Volume 2 - Systematic Reuse with ACE and Frameworks》中4.4 The ACE_WFMO_Reactor Class有说明。这儿算是借花献佛。
5.1 WFMO_Reactor只能处理62个句柄由于WaitForMultipleObjects不是一个处理大量事件的函数,其最多处理64个事件句柄,而WFMO_Reactor自身为了处理使用了2个句柄,所以一个WFMO_Rector对象只能处理。
如果你想做大规模的网络接入,62个事件句柄显然是不够的,特别是要同时处理IO事件时,导致这个不足的应该是WFMO_Reactor的设计者的一个选择。在赋予WFMO_Reactor强大的特性的同时,WFMO_Reactor的设计者只能让网络IO事件的数量委屈一下了。
5.2 WRITE_MASK触发机制WFMO_Reactor 选择的是Windows的WSAEventSelect 函数作为网络的IO的反应器。但是WSAEventSelect函数的FD_WRITE的事件处理和传统的IO反应器(select)不同。下面是MSDN的描述。
The FD_WRITE network event is handled slightly differently. An FD_WRITE network event is recorded when a socket is first connected with connect/WSAConnect or accepted with accept/WSAAccept, and then after a send fails with WSAEWOULDBLOCK and buffer space becomes available. Therefore, an application can assume that sends are possible starting from the first FD_WRITE network event setting and lasting until a send returns WSAEWOULDBLOCK. After such a failure, the application will find out that sends are again possible when an FD_WRITE network event is recorded and the associated event object is set.
简单翻译就是,只有在三种条件下,WSAEventSelect才会发出FD_WRITE通知,一是使用connect或WSAConnect,一个套接字成功建立连接后;二是使用accept或WSAAccept,套接字被接受以后;三是若send、WSASend、sendto或WSASendTo函数返回失败,而且错误是WSAEWOULDBLOCK错误后,缓冲区的空间再次变得可用时。【注】
【注】这种触发方式在IO反应器或者说IO多路复用模型中应该被称为边缘触发方式。select函数好像没有这种触发方式而是水平触发方式, Epoll是支持这种方式的,但是默认还是水平触发,这种方式可能有更高的效率,但是代码更加难写。
可以这么理解,WSAEventSelect认为套接字基本都是可写状态,它认为你应该大胆send。只有send出现WSAEWOULDBLOCK失败后,你才需要使用WSAEventSelect反应器。【注】
所以对于WFMO_Reactor的,你不可能依靠注册(或者是唤醒)IO句柄进行写操作,WMFO_Reactor很有可能不会去回调你的handle_output函数。
【注】对于网络套接字,只要缓冲区还有空间就可以直接发送,除非缓冲区没有空间了,才可能出现阻塞错误,所以直接send失败的可能性很小,另外反复调用注册IO句柄一类的操作其实是比较耗时的。其实先send,如果send失败再注册IO句柄到反应器的方式应该是一种更加高效的方式,高压力的通讯服务器应该选择这个编写方式。
我自己的通信服务器通过这个改造,提高的性能在15%左右(CPU占用率下降)。
由于WFMO_Reactor的这些特点,其实很大的限制了Reactor的可移植性。其实个人感觉如果你对系统特性没有那么多要求,在Windows下选择Select_Reactor替换WFMO_Reactor是更好的选择。
6 尽量使用ID取消ACE_Event_Handler定时器- ACE的Reactor 提供了两种方式取消定时器:virtual int cancel_timer (ACE_Event_Handler *event_handler, int dont_call_handle_close = 1);virtual int cancel_timer (long timer_id, const void **arg = 0, int dont_call_handle_close = 1);
复制代码
一种是使用定时器ID取消定时器,这个ID是定时器是的返回值,一种是采用相应的ACE_Event_Handler指针取消定时器。一般情况下使用ACE_Event_Handler的指针取消定时器无疑是最简单的方法,但是这个方法却不是一个高效的实现。所以如果您的程序有大规模的定时器设置取消操作,建议尽量使用ID取消定时器。我们用ACE_Timer_Heap和ACE_Timer_Has两个Timer_Queue剖析一下。
6.1 ACE_Timer_Heap如何根据Event_handler取消先选择最常用的Time_Queue ACE_Timer_Heap举例,其使用ACE_Event_Handler关闭定时器的代码是:
- template <class TYPE, class FUNCTOR, class ACE_LOCK> intACE_Timer_Heap_T<TYPE, FUNCTOR, ACE_LOCK>::cancel (const TYPE &type, int dont_call){ // Try to locate the ACE_Timer_Node that matches the timer_id. //循环比较所有的的ACE_Event_Handler的指针是否相同 for (size_t i = 0; i < this->cur_size_; ) { if (this->heap_->get_type () == type) { ……………… } }
复制代码而使用TIMER_ID关闭的代码如下,它是通过数组下标进行的定位操作。
- template <class TYPE, class FUNCTOR, class ACE_LOCK> intACE_Timer_Heap_T<TYPE, FUNCTOR, ACE_LOCK>::cancel (long timer_id, const void **act, int dont_call){ //通过数组下标操作,速度当然奇快无比。 ssize_t timer_node_slot = this->timer_ids_[timer_id]; …… //跟进数组ID进行操作 else { ACE_Timer_Node_T<TYPE> *temp = this->remove (timer_node_slot); }}
复制代码对于ACE_Timer_Heap,采用ACE_Event_Handler指针取消定时器的方式的平均时间复杂度应该就是O(N)。由于ACE的的一个Event_handler可能对应多个定时器,所以必须检查所有的才能确保取消所有的相关定时器。
6.2 ACE_Timer_Hash如何根据Event_handler取消对于Timer_Hash,其通过ACE_Event_Handler关闭定时器的代码是:
- template <class TYPE, class FUNCTOR, class ACE_LOCK, class BUCKET> intACE_Timer_Hash_T<TYPE, FUNCTOR, ACE_LOCK, BUCKET>::cancel (const TYPE &type, int dont_call){ Hash_Token<TYPE> **timer_ids = 0; //根据Event Handler有一个定时器new一个数组出来 ACE_NEW_RETURN (timer_ids, Hash_Token<TYPE> *[this->size_], -1); size_t pos = 0; //根据定时器的个数再进行取消 for (i = 0; i < this->table_size_; ++i) { ACE_Timer_Queue_Iterator_T<TYPE, ACE_Timer_Hash_Upcall<TYPE, FUNCTOR, ACE_LOCK>, ACE_Null_Mutex> &iter = this->table_->iter ();
复制代码可以看到Timer_Hash的cancel比ACE_Timer_Heap的cancel(Event_Handler)要好一点点。但是其中也有new和delete操作,这些操作也不是高效操作。
所以说在大规模的定时器使用中,推荐你还是使用定时器的ID取消定时器更加高效的多。
7 注意ACE_Pipe的实现ACE_Pipe是一个跨平台的管道实现。标准情况来讲,采用的实现,但是在最大的两个平台Windows和Linux上,ACE的实现是采用的Socket实现。
- intACE_Pipe:open (int buffer_size){ ACE_TRACE ("ACE_Pipe:open"); #if defined (ACE_LACKS_SOCKETPAIR) || defined (__Lynx__) //绑定了一个本地端口,0.0.0.0,然后找到相应的端口,用于后面的链接 if (acceptor.open (local_any) == -1 || acceptor.get_local_addr (my_addr) == -1) result = -1; else { // Establish a connection within the same process. if (connector.connect (writer, sv_addr) == -1) result = -1; ……
复制代码
所以很多管道特性所特有的东西,在这两个平台上是无法使用ACE_Pipe实现的。比如,管道的特性可以保证在暂时没有接受者的情况下使用,而Socket是不可能有这个特性的。你必须保证先有接受者,后有发送者的时序。
所以在这些平台上最好不用这个封装。
8 慎用Reactor Notify机制在Reactor的模式,有一种辅助的通知机制,Notify机制,简单说就是通过通知发起者调用notify函数,notify的消息被保存在一个管道中,handle_event的处理中会检查这个管道中是否有通知数据,如果有就根据通知的消息,会根据默认的通知消息的类型去调用hanle_input等函数。
从设计的角度将,这个机制无疑是非常优美的,对于Reactor,它在IO驱动以外,提供了一种新的驱动方式。但是从实现角度来讲,这个机制要慎用。原因有两个。
8.1 ACE Reactor的默认Notify方式采用的是ACE_PipeACE Reactor的默认Notify方式采用的是ACE_Pipe,所以ACE_Pipe在Windows和Linux平台上的问题,Notify机制把ACE_Pipe的缺陷一个不少的继承了,而且问题更加多。
/**
- * Contains the ACE_HANDLE the ACE_Dev_Poll_Reactor is listening * on, as well as the ACE_HANDLE that threads wanting the attention * of the ACE_Dev_Poll_Reactor will write to. */ ACE_Pipe notification_pipe_;
复制代码
原来在调试ACE代码的时候,我发现只要一使用Reactor,即使只使用定时器(除非明确不使用Notify),防火墙都会报警有监听端口。我曾经对此大惑不解,直到读了ACE的这部分原代码。这样做的坏处有很多。第一个是由于采用的阻塞IO。速度会慢很多,第二个由于是单线程的处理,如果在压力极大的情况下,可能出现死锁的问题。比如在有大规模的Notify的情况下,发送缓冲区很可能会被塞满(由于是单线程,这时不会有接受者),同时由于为了简化,ACE_Pipe采用的IO是阻塞的,所以会导致整个程序死锁。第三就是这样的情况下ACE_Pipe会打开一个临时的端口,而且会绑定所有的IP(0.0.0.0),如果对于一个安全要求严格的的场景,这个将是一个不可饶恕的错误。【注】
【注】在一个安全要求严格的环境下,这个临时端口轻则可以让你的服务器轻易陷于崩溃,重则可以让你整个网络被黑客攻陷。
不过还好的是ACE的开发者估计自己也意识倒了这个麻烦。所以提供了另外一种消息队列的方式。你可以通过定义ACE_HAS_REACTOR_NOTIFICATION_QUEUE的宏编译ACE,这样ACE将不使用ACE_Pipe作为Notify消息的管道,而使用一个自己的内存队列保存Notify消息,这个队列是动态扩展的。而且由于是内存操作,性能方面没有太大问题。
大体位置在重复编译的卫哨后面,#include /**/ "ace/pre.h"前面。保证这个宏起到作用。
- #ifndef ACE_CONFIG_LINUX_H#define ACE_CONFIG_LINUX_H //使用内存队列作为Notify Queue#define ACE_HAS_REACTOR_NOTIFICATION_QUEUE #include /**/ "ace/pre.h"
复制代码
这个问题到5.6.1还是存在的,估计由于历史的原因,在很长一段时间也不会得到解决。
8.2 考虑不周的Reactor Notify机制同上,这也应该是一个BUG,Reactor Notify的代码有考虑不周的地方。Notify机制的本质是提供了一条消息队列让大家有方法调用Event_handler,但是存在一种可能,在你的通知消息在消息队列的时候,Event_hanlder由于后面的处理可能已经handle_close了。但是ACE的dispatch_notify却没有考虑倒这一点(或者说考虑倒这一点也不好解决)。
- ACE_Select_Reactor_Notify::dispatch_notify函数的代码。intACE_Select_Reactor_Notify::dispatch_notify (ACE_Notification_Buffer &buffer){…………ACE_Event_Handler *event_handler = buffer.eh_; bool const requires_reference_counting = event_handler->reference_counting_policy ().value () == ACE_Event_Handler::Reference_Counting_Policy::ENABLED; //如果此时这个ACE_Event_Handler已经被handle_close了,你如何是好。。。。 switch (buffer.mask_) { case ACE_Event_Handler::READ_MASK: case ACE_Event_Handler::ACCEPT_MASK: result = event_handler->handle_input (ACE_INVALID_HANDLE);
复制代码
这个bug到5.6.1还没有解决。我觉得这个问题是可以解决的(暂时还没有提BUG),但是得到解决的方式却仍然是低效的方案(还记得取消定时器的那个缺陷吗)。
如果你仔细看过上面的几节,你也许会发出惊叹,啊,又是Reactor Notify?对,又是它。看起来我好像一直在和ACE的Notify机制在做对,但它的确让我吃了无数的苦头。这部分的设计的确有一点画蛇添足的感觉,而且由于跨平台性等原因,这个东东的实现一直不如意。其实自己使用ACE的实现(比如Message_Queue)一套这样的机制应该是易如反掌的事情。不苛求了。
如果你用不到Notify机制,最好在ACE_Reactor初始化的时候彻底关闭Notify机制。很多Reactor的初始化函数都提供了关闭notify pipe的方式。比如ACE_Select_Reactor_T的open函数的disable_notify_pipe参数。当其为1的时候表示关闭notify 管道。
//disable_notify_pipe参数为1时表示关闭NOTIFY PIPE,不使用他
- template <class ACE_SELECT_REACTOR_TOKEN> intACE_Select_Reactor_T<ACE_SELECT_REACTOR_TOKEN>:open (size_t size, int restart, ACE_Sig_Handler *sh, ACE_Timer_Queue *tq, int disable_notify_pipe, /* 等于==1表示关闭notify机制 */ ACE_Reactor_Notify *notify)
复制代码
9 ACE_Dev_Poll_Reactor的处理优先级严重偏向定时器不使用POLL和EPOLL【注】的人,估计不太知道这个ACE_Dev_Poll_Reactor,但实际上。特别是Linux下的EPOLL(一个IO多路服用模型),这是Linux大规模接入的重要法宝,从目前的表现来看,其他平台上还没有可以超越EPOLL的东西,Windows下的异步IO的性能也还远远逊于EPOLL。
如果要使用EPOLL而不是POLL,要使用宏ACE_HAS_EVENT_POLL编译ACE,大体位置在重复编译的卫哨后面,#include /**/ "ace/pre.h"前面。保证起到作用。
- #ifndef ACE_CONFIG_LINUX_H#define ACE_CONFIG_LINUX_H// ACE_HAS_EVENT_POLL宏用于定义使用EPOLL模块,同时注意不同LINUX平台下编译可能有少量//不同。我曾经使用过的一个内核2.4的Slackware平台,要在编译ACE的时候加入 –lepoll,可能是由于//其是打补丁增加的功能#define ACE_HAS_EVENT_POLL #include /**/ "ace/pre.h"
复制代码
但也许是由于这个东西过新还是由于设计者是一个定于时间要求很敏感的人。的设计明显的是定时器优先。但是了解EPOLL和POLL的人都知道,UNIX和Linux设计这两个咚咚的目的就是解决大规模IO复用。不是为了保证定时器优先,所以我对这个设计很是不解,郁闷。其大体思路为,
1.) 先检查定时器超时的队列,计算最小的超时时间,用于IO等待。
2.) 触发IO事件
3.) 处理超时的Handler,如果有超时的事件,返回(1)。这点我看得最郁闷。
4.) 再分发处理IO事件
可以看到在处理超时句柄的时候,ACE_Dev_Poll_Reactor发现有超时的事件会返回到检查超时队列。所以如果在Reactor同时有定时处理,IO的优先级会很低。
其实这个的设计者也知道这个问题。他在代码中间做了如下的记录。
- intACE_Dev_Poll_Reactor::dispatch (Token_Guard &guard){…… // Handle timers early since they may have higher latency // constraints than I/O handlers. Ideally, the order of // dispatching should be a strategy... if ((result = this->dispatch_timer_handler (guard)) != 0) return result;
复制代码
由于EPOLL的特性,使用它大部分都是为了处理大规模的IO请求,定时器其实只有少量的需求,不是我们需求的重点。
这个问题到最近的5.6.1版本没有得到解决。
我曾经反馈过这个问题。但是得到没有明确的解答。解决这个问题的方法其实也很简单,自己重载这个类,然后自己实现相应的函数。触发IO事件后立即分发IO事件,而且加入了一个IO的优先级别。在多次IO处理的循环后在进入时间事件处理。保证时间处理的粒度在1s以内基本就可以了。