通信人家园

 找回密码
 注册

只需一步,快速开始

短信验证,便捷登录

搜索
查看: 2983|回复: 1
打印

ACE的陷阱 [复制链接]

军衔等级:

  中士

注册:2009-4-23
跳转到指定楼层
1#
发表于 2010-4-7 17:58:32 |只看该作者 |倒序浏览
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_Hash

ACE有一个非常优美的定时器队列模型,他提供了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> 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自己的文档[url=http://www.cs.wustl.edu/%7Eschmidt/Timer_Queue.html]《Design, Performance, and Optimization of Timer Strategies for Real-time ORBs》中间也有较为正确的描述。


这个问题至少倒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剖析一下。[ i]
6.1               ACE_Timer_Heap如何根据Event_handler取消

先选择最常用的Time_Queue ACE_Timer_Heap举例,其使用ACE_Event_Handler关闭定时器的代码是:
template <class TYPE, class FUNCTOR, class ACE_LOCK> int
ACE_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_[ i]->get_type () == type)
        {
          ………………
        }
   }

而使用TIMER_ID关闭的代码如下,它是通过数组下标进行的定位操作。
template <class TYPE, class FUNCTOR, class ACE_LOCK> int
ACE_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> int
ACE_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_[ i]->iter ();

可以看到Timer_Hash的cancel比ACE_Timer_Heap的cancel(Event_Handler)要好一点点。但是其中也有new和delete操作,这些操作也不是高效操作。
所以说在大规模的定时器使用中,推荐你还是使用定时器的ID取消定时器更加高效的多。

7               注意ACE_Pipe的实现

ACE_Pipe是一个跨平台的管道实现。标准情况来讲,采用的实现,但是在最大的两个平台Windows和Linux上,ACE的实现是采用的Socket实现。
<div style="border: 1pt solid windowtext; padding: 1pt 4pt; background: none repeat scroll 0% 0% rgb(217, 217, 217);">int
ACE_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

举报本楼

本帖有 1 个回帖,您需要登录后才能浏览 登录 | 注册
您需要登录后才可以回帖 登录 | 注册 |

手机版|C114 ( 沪ICP备12002291号-1 )|联系我们 |网站地图  

GMT+8, 2024-11-16 09:37 , Processed in 0.413333 second(s), 15 queries , Gzip On.

Copyright © 1999-2023 C114 All Rights Reserved

Discuz Licensed

回顶部