ACE的陷阱

本文揭示了ACE(Adaptive Communication Environment)框架在实际使用中的一些潜在问题和陷阱,包括低效模块、设计缺陷、使用不便之处以及容易误解或误用的功能。作者指出,ACE的某些组件如ACE_Timer_Hash性能不佳,建议避免使用。此外,文章还讨论了Reactor定时器的精度、WFMO_Reactor的限制、共享内存分配以及如何更有效地管理定时器等问题。总结了使用ACE时应遵循的实践建议,如提前初始化ACE_Reactor、避免使用ACE_SOCK_Stream的自动关闭等。
摘要由CSDN通过智能技术生成

                             ACE的陷阱

坦白说,使用这个标题无非是希望能够吸引你的眼球,这篇文章的目的仅仅是为了揭示一些ACE缺陷的。文章适合的读者是对ACEADAPTIVE 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 sinACE还在不停的发展中。很多问题可能会在以后的版本中间改进。所以在我认为的的确是问题的章节后面,我会附上知道错误的版本号。

 

1               我将什么列为陷阱

1.1               低效的模块

作为一个代码级的中间件。ACE无疑是高效的,但是坦白说ACE的代码不是非常完美的。ACE的很多地方提供的是一个框架解决方案,为了保证框架的可移植和通用,代码中大量使用了virtual 函数,Bridge模式,多线程下的锁操作,甚至有相当的new操作……,这些东西都限制ACE的性能。所以个人谨慎的将ACE的效率定义为中上。

个人认为,一般情况下,如果你使用ACEAPI代替系统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自己对于宏的处理是比较严谨的,ACEconfig-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_Hash

ACE有一个非常优美的定时器队列模型,他提供了4种定时器Queue让大家使用:ACE_Timer_HeapACE_Timer_WheelACE_High_Res_TimerACE_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> int

ACE_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_[i]->is_empty ()

             && this->table_[i]->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)

 

这个问题在ACE自己的文档Design, Performance, and Optimization of Timer Strategies for Real-time ORBs中间也有较为正确的描述。

 

这个问题至少倒 5.6.1 的版本还是存在的。我个人估计也不会得到解决。Hash的特性摆在那儿呢,除非ACE采用更加复杂的数据结构。

 

4               Reactor定时器的精度取决于实现

由于Reactor在各个平台的默认实现都取决于平台的实现,比如在Windows下默认的ReactorWFMO_REACTOR,而在LinuxUNIX平台,默认的ReactorSelect_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_ReactorACE_ReactorWindows下的默认实现(为什么不选择ACE_Select_Reactor作为默认实现,可能是基于效率和强大性的考虑),WFMO_Reactor的低层使用的函数是WaitForMultipleObjectsWSAEventSelectWSAEnumNetworkEvents。其中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 选择的是WindowsWSAEventSelect 函数作为网络的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通知,一是使用connectWSAConnect,一个套接字成功建立连接后;二是使用acceptWSAAccept,套接字被接受以后;三是若sendWSASendsendtoWSASendTo函数返回失败,而且错误是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定时器

ACEReactor 提供了两种方式取消定时器:

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_HeapACE_Timer_Has两个Timer_Queue剖析一下。

6.1               ACE_Timer_Heap如何根据Event_handler取消

先选择最常用的Time_Queue ACE_Timer_Heap举例,其使用ACE_Event_Handler

评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值