`

初学者:ACE学习

 
阅读更多

转载自:初学者:ACE学习

ACE的配置(window)

(使用VC++)安装:

1. 从网上下载相应源码――――根据提示编辑config.h文件,并放置在ACE_ROOT\ace 目录下。

2. 用VC打开ACE_ROOT\ace\ace.dsw ,并编译,编译后会在ACE_ROOT\lib 目录下生成两个库:ACEd.dll(动态库)和ACEd.lib(静态库)。

3. 链接:把ACEd.dll(动态库)和ACEd.lib(静态库)复制到目录ACE_ROOT\ace下,因为这是默认的静态库链接路径。或者可以修改静态库链接路径:project->setting->link->object/library 填入ACE_ROOT\lib.

4.执行:If you use the dynamic libraries, make sure you include ACE_ROOT\bin in your PATH whenever you run programs that

uses ACE. Otherwise you may experience problems finding ace.dll or aced.dll.

就是在程序执行时,如果使用ACE的动态库,必须修改系统环境变量PATH的值,把包含ACEd.dll 的路径添加到PATH中。修改如下:电脑属性――高级――环境变量――系统变量――Path(修改)。或者把ACEd.dll 添加到系统默认的搜索路径之中,如添加到c:/window/system32 中。

在VC++下使用ACE

  新建工程:使用new菜单,选中project。选取win32 console Applitation(控制台应用程序),建立一个空项目。同时新建一个workspace。

  一个workspace可以对应几个项目,一个项目对应一个程序。当然如何在一个workspace管理多个工程现在还没搞清楚。Workspace保存空间中的相应配置,而一个项目保存,自己项目下的项目配置。

  添加头文件的搜索路径:tools->option->directories,添加ace头文件路径。当然,按照同样的办法,也可以修改库文件、执行文件、源文件的搜索路径,只需做相应选择就可以了。

  修改项目的setting:通过project->setting进入,或是直接在项目名字上点击右键,选择setting即可。把C/C++中的MLd修改为MDd(多线程);把link中input中的库改为aced.lib,同时additional path 中 http://www.cnblogs.com/../ace。这样设置就基本完成。

注:以下摘抄自《ACE 程序员教程》

ACE简介

  ACE自适配通信环境 (Adaptive Communication Environment)是面向对象的构架和工具包,它为通信软件实现了核心的并发和分布式模式。ACE中的组件可用于以下几种目的:

· 并发和同步

· 进程间通信(IPC)

· 内存管理

· 定时器

· 信号

· 文件系统管理

· 线程管理

· 事件多路分离和处理器分派

· 连接建立和服务初始化

· 软件的静态和动态配置、重配置

· 分层协议构建和流式构架

· 分布式通信服务:名字、日志、时间同步、事件路由和网络锁定,等等。

  目前ACE适用的OS平台包括:实时OS(VxWorks、Chorus、LynxOS和pSoS)、大多数版本的UNIX(SunOS 4.x和5.x; SGI IRIX 5.x和6.x; HP-UX 9.x, 10.x和11.x; DEC UNIX 3.x和4.x; AIX 3.x和4.x; DG/UX; Linux; SCO; UnixWare; NetBSD和FreeBSD)、Win32(使用MSVC++和Borland C++的WinNT 3.5.x、4.x、Win95和WinCE)以及MVS OpenEdition。

  在ACE构架中有三个基本层次:

· 操作系统(OS)适配层

· C++包装层

· 构架和模式层

第2章 IPC SAP:进程间通信服务访问点包装

  ACE_IPC_SAP类提供的一些函数是所有IPC接口公有的。有四个不同的类由此类派生而出,每个类各自代表ACE包含的一种IPC SAP包装类属。这些类封装适用于特定IPC接口的功能。例如,ACE_SOCK类包含的功能适用于BSD socket编程接口,而ACE_TLI包装TLI编程接口。ACE_FIFO类和 ACE_SPIPE类。

socket类属(ACE_SOCK)

类名

职责

ACE_SOCK_Acceptor

用于被动的连接建立,基于BSD accept()和listen()调用。

ACE_SOCK_Connector

用于主动的连接建立,基于BSD connect()调用。

ACE_SOCK_Dgram

用于提供基于UDP(用户数据报协议)的无连接消息传递服务。封装了sendto()和receivefrom()等调用,并提供了简单的send()和recv()接口。

ACE_SOCK_IO

用于提供面向连接的消息传递服务。封装了send()、recv()和write()等调用。该类是ACE_SOCK_Stream和ACE_SOCK_CODgram类的基类。

ACE_SOCK_Stream

用于提供基于TCP(传输控制协议)的面向连接的消息传递服务。派生自ACE_SOCK_IO,并提供了更多的包装方法。

ACE_SOCK_CODgram

用于提供有连接数据报(connected datagram)抽象。派生自ACE_SOCK_IO;它包含的open()方法使用bind()来绑定到指定的本地地址,并使用UDP连接到远地地址。

ACE_SOCK_Dgram_Mcast

用于提供基于数据报的多点传送(multicast)抽象。包括预订多点传送组,以及发送和接收消息的方法

ACE_SOCK_Dgram_Bcast

用于提供基于数据报的广播(broadcast)抽象。包括在子网中向所有接口广播数据报消息的方法

          表2-1 ACE_SOCK中的类及其职责

第3章 ACE的内存管理

  ACE含有两组不同的类用于内存管理。

  第一组是那些基于ACE_Allocator的类。这组类使用动态绑定和策略模式来提供灵活性和可扩展性。它们只能用于局部的动态内存分配。

  第二组类基于ACE_Malloc模板类。这组类使用C++模板和外部多态性 (External Polymorphism)来为内存分配机制提供灵活性。在这组类中的类不仅包括了用于局部动态内存管理的类,也包括了管理进程间共享内存的类。这些共享内存类使用底层OS(OS)共享内存接口。

3.1 分配器(Allocator)

  分配器用于在ACE中提供一种动态内存管理机制。在ACE中有若干使用不同策略的分配器可用。这些不同策略提供相同的功能,但是具有不同的特性。所有的分配器都支持ACE_Allocator接口,因此无论是在运行时还是在编译时,它们都可以很容易地相互替换。这也正是灵活性之所在。

分配器

描述

ACE_Allocator

ACE中的分配器类的接口类。这些类使用继承和动态绑定来提供灵活性。

ACE_Static_Allocator

该分配器管理固定大小的内存。每当收到分配内存的请求时,它就移动内部指针、以返回内存chunk(“大块”)。它还假定内存一旦被分配,就再也不会被释放。

ACE_Cached_Allocator

该分配器预先分配内存池,其中含有特定数目和大小的内存chunk。这些chunk在内部空闲表(free list)中进行维护,并在收到内存请求(malloc())时被返回。当应用调用free()时,chunk被归还到内部空闲表、而不是OS中。

ACE_New_Allocator

为C++ new和delete操作符提供包装的分配器,也就是,它在内部使用new和delete操作符,以满足动态内存请求。

              表3-1 ACE中的分配器

使用如下:

typedef ACE_Cached_Allocator<MEMORY_BLOCK,ACE_SYNCH_MUTEX> Allocator;

3.2 ACE_Malloc

  Malloc类集使用模板类ACE_Malloc来提供内存管理。ACE_Malloc模板需要两个参数(一个是内存池,一个是池锁),以产生我们的分配器类。当应用发出free()调用时,ACE_Malloc不会把所释放的内存返还给内存池,而是由它自己的空闲表进行管理。当ACE_Malloc收到后续的内存请求时,它会使用空闲表来查找可返回的空block。因而,在使用ACE_Malloc时,如果只发出简单的malloc()和free()调用,从OS分配的内存数量将只会增加,不会减少。ACE_Malloc类还含有一个remove()方法,用于发出请求给内存池,将内存返还给OS。该方法还将锁也返还给OS。

3.2.2 使用ACE_Malloc

  ACE_Malloc类的使用很简单。首先,用你选择的内存池和锁定机制实例化ACE_Malloc,以创建分配器类。随后用该分配器类实例化一个对象,这也就是你的应用将要使用的分配器。当你实例化分配器对象时,传给构造器的第一个参数是一个字符串,它是你想要分配器对象使用的底层内存池的“名字”。将正确的名字传递给构造器非常重要,特别是如果你在使用共享内存的话。否则,分配器将会为你创建一个新的内存池。如果你在使用共享内存池,这当然不是你想要的,因为你根本没有获得共享。

  为了方便底层内存池的共享(重复一次,如果你在使用共享内存的话,这是很重要的),ACE_Malloc类还拥有一个映射(map)类型接口:可被给每个被分配的内存block一个名字,从而使它们可以很容易地被在内存池中查找的另一个进程找到。该接口含有bind()和find()调用。bind()调用用于给由malloc()调用返回给ACE_Malloc的block命名。find()调用,如你可能想到的那样,用于查找与某个名字相关联的内存。

            表3-2列出了各种可用的内存池:

池名

描述

ACE_MMAP_Memory_Pool

ACE_MMAP_MEMORY_POOL

使用<mmap(2)>创建内存池。这样内存就可在进程间共享了。每次更新时,内存都被更新到后备存储(backing store)。

ACE_Lite_MMAP_Memory_Pool

ACE_LITE_MMAP_MEMORY_POOL

使用<mmap(2)>创建内存池。不像前面的映射,它不做后备存储更新。代价是较低可靠性。

ACE_Sbrk_Memory_Pool

ACE_SBRK_MEMORY_POOL

使用<sbrk(2)>调用创建内存池。

ACE_Shared_Memory_Pool

ACE_SHARED_MEMORY_POOL

使用系统V <shmget(2)>调用创建内存池。

Memory_Pool

内存可在进程间共享。

ACE_Local_Memory_Pool

ACE_LOCAL_MEMORY_POOL

通过C++的new和delete操作符创建局部内存池。该池不能在进程间共享。

第4章 线程管理:ACE的同步和线程管理机制

  ACE_Thread提供了对OS的线程调用的简单包装,这些调用处理线程创建、挂起、取消和删除等问题。它提供给应用程序员一个简单易用的接口,可以在不同的线程API间移植。ACE_Thread是非常“瘦”的包装,有着很少的开销。其大多数方法都是内联的,因而等价于对底层OS专有线程接口的直接调用。ACE_Thread中的所有方法都是静态的,而且该类一般不进行实例化。

  线程是通过使用ACE_Thread::spawn_n()调用创建的。要作为线程的执行启动点调用的函数的指针(在此例中为worker()函数)被作为参数传入该调用中。要注意的重点是ACE_Thread::spawn_n()要求所有的线程启动函数(方法)必须是静态的或全局的(就如同直接使用OS线程API时所要求的一样)。

  等待是通过使用ACE_Thread::join()调用来完成的。该方法的参数是你想要主线程与之联接的线程的句柄(ACE_hthread_t)。

4.2 ACE同步原语

  ACE有若干可用于同步目的的类。这些类可划分为以下范畴:

· ACE Lock类属

· ACE Guard类属

· ACE Condition类属

· 杂项ACE Synchronization类

名字

描述

ACE_Mutex

封装互斥机制(根据平台,可以是mutex_t、pthread_mutex_t等等)的包装类,用于提供简单而有效的机制来使对共享资源的访问序列化。它与二元信号量(binary semaphore)的功能相类似。可被用于线程和进程间的互斥。

ACE_Thread_Mutex

可用于替换ACE_Mutex,专用于线程同步。

ACE_Process_Mutex

可用于替换ACE_Mutex,专用于进程同步。

ACE_NULL_Mutex

提供了ACE_Mutex接口的“无为”(do-nothing)实现,可在不需要同步时用作替换(单线程使用)。

ACE_RW_Mutex

封装读者/作者锁的包装类。它们是分别为读和写进行获取的锁,在没有作者在写的时候,多个读者可以同时进行读取。

ACE_RW_Thread_Mutex

可用于替换ACE_RW_Mutex,专用于线程同步。

ACE_RW_Process_Mutex

可用于替换ACE_RW_Mutex,专用于进程同步。

ACE_Semaphore

这些类实现计数信号量,在有固定数量的线程可以同时访问一个资源时很有用。在OS不提供这种同步机制的情况下,可通过互斥体来进行模拟。

ACE_Thread_Semaphore

应被用于替换ACE_Semaphore,专用于线程同步。

ACE_Process_Semaphore

应被用于替换ACE_Semaphore,专用于进程同步。

ACE_Token

提供“递归互斥体”(recursive mutex),也就是,当前持有某令牌的线程可以多次重新获取它,而不会阻塞。而且,当令牌被释放时,它确保下一个正阻塞并等待此令牌的线程就是下一个被放行的线程。

ACE_Null_Token

令牌接口的“无为”(do-nothing)实现,在你知道不会出现多个线程时使用。

ACE_Lock

定义锁定接口的接口类。一个纯虚类,如果使用的话,必须承受虚函数调用开销。

ACE_Lock_Adapter

基于模板的适配器,允许将前面提到的任意一种锁定机制适配到ACE_Lock接口。

            表4-1 ACE锁类属中的类

  表4-1中描述的类都支持同样的接口。但是,在任何继承层次中,这些类都是互不关联的。在ACE中,锁通常用模板来参数化,因为,在大多数情况下,使用虚函数调用的开销都是不可接受的。使用模板使得程序员可获得相当程度的灵活性。他可以在编译时(但不是在运行时)选择他想要使用的的锁定机制的类型。然而,在某些情形中,程序员仍可能需要使用动态绑定和替换(substitution);对于这些情况,ACE提供了ACE_Lock和ACE_Lock_Adapter类。

  在临界区内完成的工作使用ACE_Thread_Mutex互斥体对象进行保护。该对象由主线程作为参数传给工作者线程。临界区控制是通过在ACE_Thread_Mutex对象上发出acquire()调用,从而获取互斥体的所有权来完成的。一旦互斥体被获取,没有其他线程能够再进入这一代码区。临界区控制是通过使用release()调用来释放的。一旦互斥体的所有权被放弃,就会唤醒所有其他在等待的线程。这些线程随即相互竞争,以获得互斥体的所有权。第一个试图获取所有权的线程会进入临界区。

使用示例:

struct Args

{

public:Args(int iterations): mutex_(),iterations_(iterations){}

ACE_Thread_Mutex mutex_;

int iterations_;

};

4.2.1.3 使用令牌(Token)

  如表4-1中所提到的,ACE_Token类提供所谓的“递归互斥体”,它可以被最初获得它的同一线程进行多次重新获取。ACE_Token类还确保所有试图获取它的线程按严格的FIFO(先进先出)顺序排序。

  递归锁允许同一线程多次获取同一个锁。线程不会因为试图获取它已经拥有的锁而死锁。这些类型的锁能在各种不同的情况下派上用场。例如,如果你用一个锁来维护跟踪流的一致性,你可能希望这个锁是递归的,因为某个方法可以调用一个跟踪例程,获取锁,被信号中断,然后再尝试获取这个跟踪锁。如果锁是非递归的,线程将会在这里锁住它自己。你会发现很多其他需要递归锁的有趣应用。重要的是要记住,你获取递归锁多少次,就必须释放它多少次。

  在SunOS 5.x上运行例4-3,释放锁的线程常常也是重新获得它的线程(大约90%的情况是这样)。但是如果你采用ACE_Token类作为锁定机制来运行这个例子,每个线程都会轮流获得令牌,然后有序地把机会让给下一个线程。

  尽管ACE_Token作为所谓的递归锁非常有用,它们实际上是更大的“令牌管理”构架的一部分。该构架允许你维护数据存储中数据的一致性。

4.2.2 ACE守卫(Guard)类属

  ACE中的守卫用于自动获取和释放锁。守卫类的对象定义一个代码块,在其上获取一个锁。在退出此代码块时,锁被自动释放。

  ACE中的守卫类是一种模板,它通过所需锁定机制的类型来参数化。底层的锁可以是ACE Lock类属中的任何类,也就是,任何互斥体或锁类。它是这样工作的:对象的构造器获取锁,析构器释放锁。表4-2列出了ACE中可用的守卫:

名字

描述

ACE_Guard

自动在底层锁上调用acquire()和release()。任何ACE Lock类属中的锁都可以作为它的模板参数传入。

ACE_Read_Guard

自动在底层锁上调用acquire()和release()。

ACE_Write_Guard

自动在底层锁上调用acquire()和release()。

            表4-2 ACE中的守卫

4.2.3 ACE条件(Condition)类属

  ACE_Condition类是针对OS条件变量原语的包装类。线程常常需要特定条件被满足才能继续它的操作。条件变量不是被用作互斥原语,而是用作特定条件已经满足的指示器。在使用条件变量时,你的程序应该完成以下步骤:

· 获取全局资源(例如,消息队列)的锁(互斥体)。

· 检查条件(例如,消息队列里有空间吗?)。

· 如果条件失败,调用条件变量的wait()方法。等待在未来条件变为真。

· 当另一线程在全局资源上执行操作时,它发信号(signal())给所有其他在此资源上测试条件的线程(例如,另一线程从消息队列中取出一个消息,然后通过条件变量发送信号,以使阻塞在wait()上的线程能够再尝试将它们的消息插入队列)。

· 在醒来之后,重新检查条件现在是否为真。如为真,则在全局资源上执行操作(例如,将消息插入全局消息队列)

  需要特别注意的是,在阻塞wait调用中之前,条件变量机制(也就是ACE_Cond)负责释放全局资源上的互斥体。如果没有进行此操作,将没有其他的线程能够在此资源上工作(该资源是条件改变的原因)。同样,一旦阻塞线程收到信号、重又醒来,它在检查条件之前会在内部重新获取锁。

  注意主线程首先获取一个互斥体,然后对条件进行测试。如果条件不为真,主线程就等待在此条件变量上。条件变量随即自动释放互斥体,并使主线程进入睡眠。条件变量总是像这样与互斥体一起使用。这是一种可如下描述的一般模式[1]:

    while( expression NOT TRUE ) wait on condition variable;

  记住条件变量不是用于互斥,而是用于我们所描述的发送信号功能。

4.2.4 杂项同步类

  除了上面描述的同步类,ACE还包括其他一些同步类,比如ACE_Barrier和ACE_Atomic_Op。

4.2.4.1 ACE中的栅栏(Barrier)

  栅栏有一个好名字,因为它恰切地描述了栅栏应做的事情。一组线程可以使用栅栏来进行共同的相互同步。组中的每个线程各自执行,直到到达栅栏,就阻塞在那里。在所有相关线程到达栅栏后,它们就全部继续它们的执行。就是说,它们一个接一个地阻塞,等待其他的线程到达栅栏;一旦所有线程都到达了它们的执行路径中的“栅栏点”,它们就一起重新启动。

  在ACE中,栅栏在ACE_Barrier类中实现。在栅栏对象被实例化时,它将要等待的线程的数目会作为参数传入。一旦到达执行路径中的“栅栏点”,每个线程都在栅栏对象上发出wait()调用。它们在这里阻塞,直到其他线程到达它们各自的“栅栏点”,然后再一起继续执行。当栅栏从相关线程那里接收了适当数目的wait()调用时,它就同时唤醒所有阻塞的线程。

4.2.4.2 原子操作(Atomic Op)

  ACE_Atomic_Op类用于将同步透明地参数化进基本的算术运算中。ACE_Atomic_Op是一种模板类,锁定机制和需要参数化的类型被作为参数传入其中。ACE是这样来实现此机制的:重载所有算术操作符,并确保在操作前获取锁,在操作后释放它。运算本身被委托给通过模板传入的的类。

4.3 使用ACE_THREAD_MANAGER进行线程管理

  我们可以使用ACE_Thread包装类来创建和销毁线程。但是,该包装类的功能比较有限。ACE_Thread_Manager提供了ACE_Thread中的功能的超集。特别地,它增加了管理功能,以使启动、取消、挂起和恢复一组相关线程变得更为容易。它用于创建和销毁成组的线程和任务(ACE_Task是一种比线程更高级的构造,可在ACE中用于进行多线程编程。我们将在后面再来讨论任务)。它还提供了这样的功能:发送信号给一组线程,或是在一组线程上等待,而不是像我们在前面的例子中所看到的那样,以一种不可移植的方式来调用join()。

4.4 线程专有存储(Thread Specific Storage)

  对于各个线程来说,可能需要不同的全局或静态数据。可使用线程专有存储来满足此需求。像输入端口这样的结构可放在线程专有存储中,并可像逻辑上的静态或全局变量一样被访问;而实际上它对线程来说是私有的。

  传统上,线程专有存储通过让人迷惑的底层操作系统API来实现。在ACE中,TSS通过使用ACE_TSS模板类来实现。需要成为线程专有的类被传入ACE_TSS模板,然后可以使用C++的->操作符来调用它的全部公共方法。

第5章 任务和主动对象(Active Object):并发编程模式(多线程)

5.1 主动对象

  那么到底什么是主动对象呢?传统上,所有的对象都是被动的代码段,对象中的代码是在对它发出方法调用的线程中执行的。也就是,调用线程(calling threads)被“借出”,以执行被动对象的方法。

  而主动对象却不一样。这些对象持有它们自己的线程(甚或多个线程),并将这个线程用于执行对它们的任何方法的调用。因而,如果你想象一个传统对象,在里面封装了一个线程(或多个线程),你就得到了一个主动对象。

  例如,设想对象“A”已在你的程序的main()函数中被实例化。当你的程序启动时,OS创建一个线程,以从main()函数开始执行。如果你调用对象A的任何方法,该线程将“流过”那个方法,并执行其中的代码。一旦执行完成,该线程返回调用该方法的点并继续它的执行。但是,如果”A”是主动对象,事情就不是这样了。在这种情况下,主线程不会被主动对象借用。相反,当”A”的方法被调用时,方法的执行发生在主动对象持有的线程中。另一种思考方法:如果调用的是被动对象的方法(常规对象),调用会阻塞(同步的);而另一方面,如果调用的是主动对象的方法,调用不会阻塞(异步的)。

5.2 ACE_Task

  ACE_Task是ACE中的任务或主动对象“处理结构”的基类。在ACE中使用了此类来实现主动对象模式。所有希望成为“主动对象”的对象都必须从此类派生。你也可以把ACE_TASK看作是更高级的、更为面向对象的线程类。

  当我们在前一章中使用ACE_Thread包装时,你一定已经注意到了一些“不好”之处。那一章中的大多数程序都被分解为函数、而不是对象。这是因为ACE_Thread包装需要一个全局函数名、或是静态方法作为参数。随后该函数(静态方法)就被用作所派生的线程的“启动点”。这自然就使得程序员要为每个线程写一个函数。如我们已经看到的,这可能会导致非面向对象的程序分解。

  相反,ACE_Task处理的是对象,因而在构造OO程序时更便于思考。因此,在大多数情况下,当你需要构建多线程程序时,较好的选择是使用ACE_Task的子类。这样做有若干好处。首要的是刚刚所提到的,这可以产生更好的OO软件。其次,你不必操心你的线程入口是否是静态的,因为ACE_Task的入口是一个常规的成员函数。而且,我们会看到ACE_Task还包括了一种用于与其他任务进行通信的易于使用的机制。

  重申刚才所说的,ACE_Task可用作:

· 更高级的线程(我们称之为任务)。

· 主动对象模式中的主动对象。

  ACE_Task的结构:每个任务都含有一或多个线程,以及一个底层消息队列。各个任务通过这些消息队列进行通信。但是,消息队列并非是程序员需要关注的对象。发送任务可以使用putq()调用来将消息插入到另一任务的消息队列中。随后接收任务就可以通过使用getq()调用来从它自己的消息队列里将消息提取出来。

5.2.2 创建和使用任务

  要创建任务或主动对象,必须从ACE_Task类派生子类。在子类派生之后,必须采取以下步骤:

· 实现服务初始化和终止方法:open()方法应该包含所有专属于任务的初始化代码。其中可能包括诸如连接控制块、锁和内存这样的资源。close()方法是相应的终止方法。

· 调用启用(Activation)方法:在主动对象实例化后,你必须通过调用activate()启用它。要在主动对象中创建的线程的数目,以及其他一些参数,被传递给activate()方法。activate()方法会使svc()方法成为所有它生成的线程的启动点。

· 实现服务专有的处理方法:如上面所提到的,在主动对象被启用后,各个新线程在svc()方法中启动(如何区分并调用不同线程)。应用开发者必须在子类中定义此方法。

5.2.3 任务间通信

  如前面所提到的,ACE中的每个任务都有一个底层消息队列。这个消息队列被用作任务间通信的一种方法。当一个任务想要与另一任务“谈话”时,它创建一个消息,并将此消息放入(putq())它想要与之谈话的任务的消息队列。接收任务通常用getq()从消息队列里获取消息。如果队列中没有数据可用,它就进入休眠状态。如果有其他任务将消息插入它的队列,它就会苏醒过来,从队列中拾取数据并处理它。因而,在这种情况下,接收任务将从发送任务那里接收消息,并以应用特定的方式作出反馈。

5.3 主动对象模式(Active Object Pattern)

  主动对象模式用于降低方法执行和方法调用之间的耦合。该模式描述了另外一种更为透明的任务间通信方法。

  该模式使用ACE_Task类作为主动对象。在这个对象上调用方法时,它就像是常规对象一样。就是说,方法调用是通过同样的->操作符来完成的,其不同在于这些方法的执行发生于封装在ACE_Task中的线程内。在使用被动或主动对象进行编程时,客户程序看不到什么区别,或仅仅是很小的区别。对于构架开发者来说,这是非常有用的,因为开发者需要使构架客户与构架的内部结构屏蔽开来。这样构架用户就不必去担心线程、同步、会合点(rendezvous),等等。

5.3.1 主动对象模式工作原理

  主动对象模式是ACE实现的较为复杂的模式中的一个。该模式有如下参与者:

1. 主动对象(基于ACE_Task)。

2. ACE_Activation_Queue。

3. 若干ACE_Method_Object(主动对象的每个方法都需要有一个方法对象)。

4. 若干ACE_Future对象(每个要返回结果的方法都需要这样一个对象)。

  我们已经看到,ACE_Task是怎样创建和封装线程的。要使ACE_Task成为主动对象,需要完成一些额外的工作:

  必须为所有要从客户异步调用的方法编写方法对象。每个方法对象都派生自ACE_Method_Object,并会实现它的call()方法。每个方法对象还维护上下文信息(比如执行方法所需的参数,以及用于获取返回值的ACE_Future对象。这些值作为私有属性维护)。你可以把方法对象看作是方法调用的“罩子”(closure)。客户发出方法调用,使得相应的方法对象被实例化,并被放入启用队列(activation queue)中。方法对象是命令(Command)模式的一种形式(参见有关设计模式的参考文献)。

  ACE_Activation_Queue是一个队列,方法对象在等待执行时被放入其中。因而启用队列中含有所有等待调用的方法(以方法对象的形式)。封装在ACE_Task中的线程保持阻塞,等待任何方法对象被放入启用队列。一旦有方法对象被放入,任务就将该方法对象取出,并调用它的call()方法。call()方法应该随即调用该方法在ACE_Task中的相应实现。在方法实现返回后,call()方法在ACE_Future对象中设置(set())所获得的结果。

  客户使用ACE_Future对象获取它在主动对象上发出的任何异步操作的结果。一旦客户发出异步调用,立即就会返回一个ACE_Future对象。于是客户就可以在任何它喜欢的时候去尝试从ACE_Future对象中获取结果。如果客户试图在结果被设置之前从ACE_Future对象中提取结果,客户将会阻塞。如果客户不希望阻塞,它可以通过使用ready()调用来轮询(poll)ACE_Future对象。如果结果已被设置,该方法返回1;否则就返回0。ACE_Future对象基于“多态期货”(polymorphic futures)的概念。

  call()方法的实现应该将返回的ACE_Future对象的内部值设置为从调用实际的方法实现所获得的结果(这个实际的方法实现在ACE_Task中编写)。

第6章 反应堆(Reactor):用于事件多路分离和分派的体系结构模式(事件驱动-异步事件)

  反应堆本质上提供一组更高级的编程抽象,简化了事件驱动的分布式应用的设计和实现。除此而外,反应堆还将若干不同种类的事件的多路分离集成到易于使用的API中。特别地,反应堆对基于定时器的事件、信号事件、基于I/O端口监控的事件和用户定义的通知进行统一地处理。

  ACE中的反应堆与若干内部和外部组件协同工作。其基本概念是反应堆构架检测事件的发生(通过在OS事件多路分离接口上进行侦听),并发出对预登记事件处理器(event handler)对象中的方法的“回调”(callback)。该方法由应用开发者实现,其中含有应用处理此事件的特定代码。于是用户(也就是,应用开发者)必须:

1. 创建事件处理器,以处理他所感兴趣的某事件。

2. 在反应堆上登记,通知说他有兴趣处理某事件,同时传递他想要用以处理此事件的事件处理器的指针给反应堆。

  随后反应堆构架将自动地:

1. 在内部维护一些表,将不同的事件类型与事件处理器对象关联起来。

2. 在用户已登记的某个事件发生时,反应堆发出对处理器中相应方法的回调。

6.2 事件处理器

  反应堆模式在ACE中被实现为ACE_Reactor类,它提供反应堆构架的功能接口。

  如上面所提到的,反应堆将事件处理器对象作为服务提供者使用。一旦反应堆成功地多路分离和分派了某事件,事件处理器对象就对它进行处理。因此,反应堆会在内部记住当特定类型的事件发生时,应该回调哪一个事件处理器对象。当应用在反应堆上登记它的处理器对象,以处理特定类型的事件时,反应堆会创建这种事件和相应的事件处理器的关联。

  因为反应堆需要记录哪一个事件处理器将被回调,它需要知道所有事件处理器对象的类型。这是通过替换模式(Substitution Pattern)的帮助来实现的(或者换句话说,通过“是……类型”(is a type of)变种继承)。该构架提供名为ACE_Event_Handler的抽象接口类,所有应用特有的事件处理器都必须由此派生(这使得应用特有的处理器都具有相同的类型,即ACE_Event_Handler,所以它们可以相互替换)。

ACE_Event_Handler类拥有若干不同的“handle”(处理)方法,每个处理方法被用于处理不同种类的事件。当应用程序员对特定事件感兴趣时,他就对ACE_Event_Handler类进行子类化,并实现他感兴趣的处理方法。如上面所提到的,随后他就在反应堆上为特定事件“登记”他的事件处理器类。于是反应堆就会保证在此事件发生时,自动回调在适当的事件处理器对象中的适当的”handle”方法。

  使用ACE_Reactor基本上有三个步骤:

· 创建ACE_Event_Handler的子类,并在其中实现适当的“handle_”方法,以处理你想要此事件处理器为之服务的事件类型。(参看表6-1来确定你需要实现哪一个“handle_”方法。注意你可以使用同一个事件处理器对象处理多种类型的事件,因而可以重载多于一个的“handle_”方法。)

· 通过调用反应堆对象的register_handler(),将你的事件处理器登记到反应堆。

· 在事件发生时,反应堆将自动回调相应的事件处理器对象的适当的“handle_”方法。

ACE_Event_Handler中的处理方法

在子类中重载,所处理事件的类型:

handle_signal()

信号。当任何在反应堆上登记的信号发生时,反应堆自动回调该方法。

handle_input()

来自I/O设备的输入。当I/O句柄(比如UNIX中的文件描述符)上的输入可用时,反应堆自动回调该方法。

handle_exception()

异常事件。当已在反应堆上登记的异常事件发生时(例如,如果收到SIGURG(紧急信号)),反应堆自动回调该方法。

handle_timeout()

定时器。当任何已登记的定时器超时的时候,反应堆自动回调该方法。

handle_output()

I/O设备输出。当I/O设备的输出队列有可用空间时,反应堆自动回调该方法。

      表6-1 ACE_Event_Handler中的处理方法及其对应事件

6.2.1 事件处理器登记

  登记事件处理器、以处理特定事件,是在反应堆上调用register_handler()方法来完成的。register_handler()方法是重载方法,就是说,实际上有若干方法可用于登记不同的事件类型,每个方法都叫做register_handler()。但是它们有着不同的特征:它们的参数各不相同。基本上,register_handler()方法采用handle/event_handle元组或signal/event_handler元组作为参数,并将它们加入反应堆的内部分派表。当有事件在handle上发生时,反应堆在它的内部分派表中查找相应的event_handler,并自动在它找到的event_handler上回调适当的方法。

6.2.2 事件处理器的拆除和生存期管理

  一旦所需的事件被处理后,可能就无需再让事件处理器登记在反应堆上。因而,反应堆提供了从它的内部分派表中拆除事件处理器的技术。一旦事件处理器被拆除,它就不再会被反应堆回调。把这样的死掉的句柄从反应堆里拆除是很重要的,因为,如果不这样做,反应堆将会把此句柄标记为“读就绪”,并会持续不断地回调此事件处理器的handle_方法。

6.2.2.1 从反应堆内部分派表中隐式拆除事件处理器

  隐式拆除是更为常用的从反应堆中拆除事件处理器的技术。事件处理器的每个“handle_”方法都会返回一个整数给反应堆。如果此整数为0,在处理器方法完成后、事件处理器将保持在反应堆上的登记。但是,如果“handle_”方法返回的整数<0,反应堆将自动回调此事件处理器的handle_close()方法,并将它从自己的内部分派表中拆除。handle_close()方法用于执行处理器特有的任何清除工作,它们需要在事件处理器被拆除前完成;其中可以包括像删除处理器申请的动态内存、或关闭日志文件这样的工作

6.2.2.2从反应堆内部分派表中显式拆除事件处理器

  另一种从反应堆的内部表中拆除事件处理器的方法是显式地调用反应堆的remove_handler()方法集。该方法也是重载方法,就像register_handler()一样。它采用需要拆除的处理器的句柄或信号号码作为参数,并将该处理器从反应堆的内部分派表中拆除。在remove_handler()被调用时,反应堆还自动调用事件处理器的handle_close()方法。可以这样来对其进行控制:将ACE_Event_Handler::DONT_CALL掩码传给remove_handler(),从而使得handle_close()方法不会被调用。

6.3 通过反应堆进行事件处理

6.3.1 I/O事件多路分离

  通过在具体的事件处理器类中重载handle_input()方法,反应堆可用于处理基于I/O设备的输入事件。这样的I/O可以发生在磁盘文件、管道、FIFO或网络socket上。为了进行基于I/O设备的事件处理,反应堆在内部使用从操作系统获取的设备句柄(在基于UNIX的系统中,该句柄是在文件或socket打开时,OS返回的文件描述符。在Windows中该局柄是由Windows返回的设备句柄)。网络应用显然是最适于这样的多路分离的应用之一。下面的例子演示反应堆是怎样与具体接受器一起使用来构造一个服务器的。

  第一个具体事件处理器My_Accept_Handler用于接受和建立从客户到来的连接。另一个事件处理器是My_Input_Handler,它用于在连接建立后对连接进行处理。因而,My_Accept_Handler接受连接,并将实际的处理委托给My_Input_Handler。

  我们首先创建了一个ACE_INET_Addr地址对象,将我们希望在其上接受连接的端口作为参数传给它。其次,实例化一个类型为My_Accept_Handler的对象。随后地址对象通过My_Accept_Handler的构造器传递给它。My_Accept_Handler有一个用于连接建立的底层“具体接受器”(在讲述“IPC”的一章中有与具体接受器相关的内容)。My_Accept_Handler的构造器将对新连接的“侦听”委托给该具体接受器的open()方法。在处理器开始侦听连接后,它在反应堆上登记,通知说在接收到新连接请求时,它需要被回调。为完成此操作,我们采用ACE_Event_Handler::ACCEPT_MASK掩码调用register_handler()。

  当反应堆被告知要登记处理器时,它执行“双重分派”来确定事件处理器的底层句柄。为完成此操作,它调用get_handler()方法。因为反应堆使用get_handle()方法来确定底层流的句柄,在My_Accept_Handler中必须实现get_handle()方法。在此例中,我们简单地调用具体接受器的get_handle(),它会将适当的句柄返回给反应堆。

  一旦在该句柄上接收到新的连接请求,反应堆会自动地回调My_Accept_Handler的handle_input()方法。随后Accept Handler(接受处理器)实例化一个新的Input Handler(输入处理器),并调用具体接受器的accept()方法来实际地建立连接。注意Input Handler底层的流是作为accept()调用的第一个参数传入的。这使得新实例化的Input Handler中的流被设置为在连接建立(由accept()完成)后立即创建的新流。随后Accept Handler将Input Handler登记到反应堆,通知它如果有任何可读的输入就进行回调(使用ACE_Event_Handler::READ_MASK)。随后接受处理器返回-1,使自己从反应堆的内部事件分派表中被拆除。

  现在,如果有任何输入从客户到达,反应堆将自动回调My_Input_Handler::handle_input()。注意在My_Input_Handler的handle_input()方法中,返回给反应堆是0。这指示我们希望保持它的登记;反之在My_Accept_Handler中我们在它的handle_input()中返回-1,以确保它被注销。

  除了在上面的例子中使用的READ_MASK和ACCEPT_MASK而外,还有若干其他的掩码,可在登记或是拆除处理器时使用。这些掩码如表6-2所示,它们可与register_handler()和remove_handler()方法一起使用。每个掩码保证反应堆回调事件处理器时的不同行为方式,通常这意味着不同的“handle”方法会被回调。

掩码

回调方法

何时

和……一起使用

ACE_Event_Handler::READ_MASK

handle_input()

在句柄上有数据可读时。

register_handler()

ACE_Event_Handler::WRITE_MASK

handle_output()

在I/O设备输出缓冲区上有可用空间、并且新数据可以发送给它时。

register_handler()

ACE_Event_Handler::TIMER_MASK

handle_close()

传给handle_close()以指示调用它的原因是超时。

接受器和连接器的handle_timeout方法。反应堆不使用此掩码。

ACE_Event_Handler::ACCEPT_MASK

handle_input()

在OS内部的侦听队列上收到了客户的新连接请求时。

register_handler()

ACE_Event_Handler::CONNECT_MASK

handle_input()

在连接已经建立时。

register_handler()

ACE_Event_Handler::DONT_CALL

None.

在反应堆的remove_handler()被调用时保证事件处理器的handle_close()方法不被调用。

remove_handler()

                表6-2 反应堆中的掩码

6.4 定时器(Timer)

  反应堆还包括了调度定时器的方法,它们在超时的时候回调适当的事件处理器的handle_timeout()方法。为调度这样的定时器,反应堆拥有一个schedule_timer()方法。该方法接收事件处理器(该事件处理器的handle_timeout()方法将会被回调)、以及以ACE_Time_value对象形式出现的延迟作为参数。此外,还可以指定时间间隔,使定时器在它超时后自动被复位。

  反应堆在内部维护ACE_Timer_Queue,它以定时器要被调度的顺序对它们进行维护。实际使用的用于保存定时器的数据结构可以通过反应堆的set_timer_queue()方法进行改变。反应堆有若干不同的定时器结构可用,包括定时器轮(timer wheel)、定时器堆(timer heap)和哈希式定时器轮(hashed timer wheel)。这些内容将在后面的部分详细讨论。

6.4.1 ACE_Time_Value

  ACE_Time_Value是封装底层OS平台的日期和时间结构的包装类。它基于在大多数UNIX操作系统上都可用的timeval结构;该结构存储以秒和微秒计算的绝对时间。

  其他的OS平台,比如POSIX和Win32,使用略有不同的表示方法。该类封装这些不同,并提供了可移植的C++接口。

  ACE_Time_Value类使用运算符重载,提供简单的算术加、减和比较。该类中的方法会对时间量进行“规范化”(normalize)。所谓规范化,是将timeval结构中的两个域调整为规范化的编码方式;这种编码方式可以确保精确的比较

首先通过实现事件处理器Time_Handler的handle_timeout()方法,将其设置用以处理超时。主函数实例化Time_Handler类型的对象,并使用反应堆的schedule_timer()方法调度多个定时器(10个)。handle_timeout方法需要以下参数:指向将被回调的处理器的指针、定时器超时时间,以及一个将在handle_timeout()方法被回调时发送给它的参数。每次调用schedule_timer(),它都返回一个唯一的定时器标识符,并随即存储在timer_id[]数组里。这个标识符可用于在任何时候取消该定时器。在上面的例子中也演示了定时器的取消:在所有定时器被初始调度后,程序通过调用反应堆的cancel_timer()方法(使用相应的timer_id作为参数)取消了第五个定时器。

6.4.3 使用不同的定时器队列

  不同的环境可能需要不同的调度和取消定时器的方法。在下面的任一条件为真时,实现定时器的算法的性能就会成为一个问题:

· 需要细粒度的定时器。

· 在某一时刻未完成的定时器的数目可能会非常大。

· 算法使用过于昂贵的硬件中断来实现。

  ACE允许用户从若干在ACE中已存在的定时器中进行选择,或是根据为定时器定义的接口开发他们自己的定时器。表6-3详细列出了ACE中可用的各种定时器: 

定时器

数据结构描述

性能

ACE_Timer_Heap

定时器存储在优先级队列的堆实现中。

schedule_timer()的开销=O(lg n)

cancel_timer()的开销=O(lg n)

查找当前定时器的开销=O(1)

ACE_Timer_List

定时器存储在双向链表中。

schedule_timer()的开销=O(n)

cancel_timer()的开销=O(1)

查找当前定时器的开销=O(1)

ACE_Timer_Hash

在这里使用的这种结构是定时器轮算法的变种。性能高度依赖于所用的哈希函数。

schedule_timer()的开销=最坏=O(n) 最佳=O(1)

cancel_timer()的开销=O(1)

查找当前定时器的开销=O(1)

ACE_Timer_Wheel

定时器存储在“数组指针”(pointers to arrays)的数组中。每个被指向的数组都已排序。

schedule_timer()的开销=最坏=O(n)

cancel_timer()的开销=O(1)

查找当前定时器的开销=O(1)

            表6-3 ACE中的定时器

6.5 处理信号(Signal)

  如我们在例6-1中所看到的,反应堆含有进行信号处理的方法。处理信号的事件处理器应重载handle_signal()方法,因为该方法将在信号发生时被回调。要为信号登记处理器,可以使用多个register_handler()方法中的一个,就如同例6-1中所演示的那样。如果对特定信号不再感兴趣,通过调用remove_handler(),处理器可以被拆除,并恢复为先前安装的信号处理器。反应堆在内部使用sigaction()系统调用来设置和恢复信号处理器。通过使用ACE_Sig_Handlers类和与其相关联的方法,无需反应堆也可以进行信号处理。

  使用反应堆进行信号处理和使用ACE_Sig_Handlers类的重要区别是基于反应堆的机制只允许应用给每个信号关联一个事件处理器,而ACE_Sig_Handlers类允许在信号发生时,回调多个事件处理器。

6.6 使用通知(Notification)

  反应堆不仅可以在系统事件发生时发出回调,也可以在用户定义的事件发生时回调处理器。这是通过反应堆的“通知”接口来完成的;该接口由两个方法组成:notify()和max_notify_iterations()。

  通过使用notify()方法,可以明确地指示反应堆对特定的事件处理器对象发出回调。在反应堆与消息队列、或是协作任务协同使用时,这是十分有用的。可在ASX构架组件与反应堆一起使用时找到这种用法的一些好例子。

max_notify_iterations()方法通知反应堆,每次只完成指定次数的“迭代”(iterations)。也就是说,在一次handle_events()调用中只处理指定数目的“通知”。因而如果使用max_notify_iterations()将迭代的次数设置为20,而又有25个通知同时到达,handle_events()方法一次将只处理这些通知中的20个。剩下的五个通知将在handle_events()下一次在事件循环中被调用时再处理。

  事件处理循环中值得注意的一个主要区别是,程序传递给handle_events()一个ACE_Time_Value。如果在此时间内没有事件发生,handle_events()方法就会结束。在handle_events()结束后,perform_notification()被调用,它使用反应堆的notify()方法来请求反应堆通知处理器(它是在事件发生时被作为参数传入的)。随后反应堆就使用所收到的掩码来执行对处理器的适当“handle”方法的调用。在此例中,通过传递ACE_Event_Handler::READ_MASK,我们使用notify()来通知我们的事件处理器有输入,从而使得反应堆回调该处理器的handle_input()方法。因为我们已将max_notify_iterations设为5,所以在一次handle_events()调用过程中反应堆实际上只会发出5个通知。

第7章 接受器(Acceptor)和连接器(Connector):连接建立模式

  接受器/连接器模式设计用于降低连接建立与连接建立后所执行的服务之间的耦合。因为该模式降低了服务和连接建立方法之间的耦合,非常容易改动其中一个,而不影响另外一个,从而也就可以复用以前编写的连接建立机制和服务例程的代码。

7.1 接受器模式

  在ACE中,接收器模式借助名为ACE_Acceptor的“工厂”(Factory)实现。工厂(通常)是用于对助手对象的实例化过程进行抽象的类。在面向对象设计中,复杂的类常常会将特定功能委托给助手类。复杂的类对助手的创建和委托必须很灵活。这种灵活性是在工厂的帮助下获得的。工厂允许一个对象通过改变它所委托的对象来改变它的底层策略,而工厂提供给应用的接口却是一样的,这样,可能根本就无需对客户代码进行改动(有关工厂的更多信息,请阅读“设计模式”中的参考文献)。

  ACE_Acceptor工厂允许应用开发者改变“助手”对象,以用于:

· 被动连接建立

· 连接建立后的处理

  同样地,ACE_Connector工厂允许应用开发者改变“助手”对象,以用于:

· 主动连接建立

· 连接建立后的处理

  ACE_Acceptor被实现为模板容器,通过两个类作为实参来进行实例化。第一个参数实现特定的服务(类型为ACE_Event_Handler。因为它被用于处理I/O事件,所以必须来自事件处理类层次),应用在建立连接后执行该服务;第二个参数是“具体的”接受器(可以是在IPC_SAP一章中讨论的各种变种)。

  特别要注意的是ACE_Acceptor工厂和底层所用的具体接受器是非常不同的。具体接受器可完全独立于ACE_Acceptor工厂使用,而无需涉及我们在这里讨论的接受器模式(独立使用接受器已在IPC_SAP一章中讨论和演示)。ACE_Acceptor工厂内在于接受器模式,并且不能在没有底层具体接受器的情况下使用。ACE_Acceptor使用底层的具体接受器来建立连接。如我们已看到的,有若干ACE的类可被用作ACE_Acceptor工厂模板的第二个参数(也就是,具体接受器类)。但是服务处理类必须由应用开发者来实现,而且其类型必须是ACE_Event_Handler。ACE_Acceptor工厂可以这样来实例化: 

  typedef ACE_Acceptor<My_Service_Handler,ACE_SOCK_ACCEPTOR> MyAcceptor;

  这里,名为My_Service_Handler的事件处理器和具体接受器ACE_SOCK_ACCEPTOR被传给MyAcceptor。ACE_SOCK_ACCEPTOR是基于BSD socket流家族的TCP接受器(各种可传给接受器工厂的不同类型的接受器,见表7-1和IPC一章)。请再次注意,在使用接受器模式时,我们总是处理两个接受器:名为ACE_Acceptor的工厂接受器,和ACE中的某种具体接受器,比如ACE_SOCK_ACCEPTOR(你可以创建自定义的具体接受器来取代ACE_SOCK_ACCEPTOR,但你将很可能无需改变ACE_Acceptor工厂类中的任何东西)。

  重要提示:ACE_SOCK_ACCEPTOR实际上是一个宏,其定义为: 

  #define ACE_SOCK_ACCEPTOR ACE_SOCK_Acceptor, ACE_INET_Addr

  我们认为这个宏的使用是必要的,因为在类中的typedefs在某些平台上无法工作。如果不是这样的话,ACE_Acceptor就可以这样来实例化: 

  typedef ACE_Acceptor<My_Service_Handler,ACE_SOCK_Acceptor>MyAcceptor;

7.1.1 组件

  如上面的讨论所说明的,在接受器模式中有三个主要的参与类:

  · 具体接受器:它含有建立连接的特定策略,连接与底层的传输协议机制系在一起。下面是在ACE中的各种具体接受器的例子:ACE_SOCK_ACCEPTOR(使用TCP来建立连接)、ACE_LSOCK_ACCEPTOR(使用UNIX域socket来建立连接),等等。

  · 具体服务处理器:由应用开发者编写,它的open()方法在连接建立后被自动回调。接受器构架假定服务处理类的类型是    ACE_Event_Handler,这是ACE定义的接口类(该类已在反应堆一章中详细讨论过)。另一个特别为接受器和连接器模式的服务处理而创建的类是ACE_Svc_Handler。该类不仅基于ACE_Event_Handler接口(这是使用反应堆所必需的),同时还基于在ASX流构架中使用的ACE_Task类。ACE_Task类提供的功能有:创建分离的线程、使用消息队列来存储到来的数据消息、并发地处理它们,以及其他一些有用的功能。如果与接受器模式一起使用的具体服务处理器派生自ACE_Svc_Handler、而不是ACE_Event_Handler,它就可以获得这些额外的功能。对ACE_Svc_Handler中的额外功能的使用,在这一章的高级课程里详细讨论。在下面的讨论中,我们将使用ACE_Svc_Handler作为我们的事件处理器。在简单的ACE_Event_Handler和ACE_Svc_Handler类之间的重要区别是,后者拥有一个底层通信流组件。这个流在ACE_Svc_Handler模板被实例化的时候设置。而在使用ACE_Event_Handler的情况下,我们必须自己增加I/O通信端点(也就是,流对象),作为事件处理器的私有数据成员。因而,在这样的情况下,应用开发者应该将他的服务处理器创建为ACE_Svc_Handler类的子类,并首先实现将被构架自动回调的open()方法。此外,因为ACE_Svc_Handler是一个模板,通信流组件和锁定机制是作为模板参数被传入的。

  · 反应堆:与ACE_Acceptor协同使用。如我们将看到的,在实例化接受器后,我们启动反应堆的事件处理循环。反应堆,如先前所解释的,是一个事件分派类;而在此情况下,它被接受器用于将连接建立事件分派到适当的服务处理例程。

接受器类型

所用地址

所用流

具体接受器

TCP流接受器

ACE_INET_Addr

ACE_SOCK_STREAM

ACE_SOCK_ACCEPTOR

UNIX域本地流socket接受器

ACE_UNIX_Addr

ACE_LSOCK_STREAM

ACE_LSOCK_ACCEPTOR

管道作为底层通信机制

ACE_SPIPE_Addr

ACE_SPIPE_STREAM

ACE_SPIPE_ACCEPTOR

          表7-1 ACE中的连接建立机制

7.2 连接器

  连接器与接受器非常类似。它也是一个工厂,但却是用于主动地连接远程主机。在连接建立后,它将自动回调适当的服务处理对象的open()方法。连接器通常被用在你本来会使用BSD connect()调用的地方。在ACE中,连接器,就如同接受器,被实现为名为ACE_Connector的模板容器类。如先前所提到的,它需要两个参数,第一个是事件处理器类,它在连接建立时被调用;第二个是“具体的”连接器类。

  你必须注意,底层的具体连接器和ACE_Connector工厂是非常不一样的。ACE_Connector工厂使用底层的具体连接器来建立连接。随后ACE_Connector工厂使用适当的事件或服务处理例程(通过模板参数传入)来在具体的连接器建立起连接之后处理新连接。如我们在IPC一章中看到的,没有ACE_Connector工厂,也可以使用这个具体的连接器。但是,没有具体的连接器类,就会无法使用ACE_Connector工厂(因为要由具体的连接器类来实际处理连接建立)。

  下面是对ACE_Connector类进行实例化的一个例子:

  typedef ACE_Connector<My_Svc_Handler,ACE_SOCK_CONNECTOR> MyConnector;

这个例子中的第二个参数是具体连接器类ACE_SOCK_CONNECTOR。连接器和接受器模式一样,在内部使用反应堆来在连接建立后回调服务处理器的open()方法。我们可以复用我们为前面的接受器例子所写的服务处理例程。

7.3 高级课程

  下面的部分更为详细地解释接受器和连接器模式实际上是如何工作的。如果你想要调谐服务处理和连接建立策略(其中包括调谐底层具体连接器将要使用的服务处理例程的创建和并发策略,以及连接建立策略),对该模式的进一步了解就是必要的。此外,还有一部分内容解释怎样使用通过ACE_Svc_Handler类自动获得的高级特性。最后,我们说明怎样与接受器和连接器模式一起使用简单的轻量级ACE_Event_Handler。

7.3.1 ACE_SVC_HANDLER类

  如上面所提到的,ACE_Svc_Handler类基于ACE_Task(它是ASX流构架的一部分)和ACE_Event_Handler接口类。因而ACE_Svc_Handler既是任务,又是事件处理器。这里我们将简要介绍ACE_Task和ACE_Svc_Handler的功能。

7.3.1.1 ACE_Task

  ACE_Task被设计为与ASX流构架一起使用;ASX基于UNIX系统V中的流机制。在设计上ASX与Larry Peterson构建的X-kernel协议工具非常类似。

  ASX的基本概念是:到来的消息会被分配给由若干模块(module)组成的流。每个模块在到来的消息上执行某种固定操作,然后把它传递给下一个模块作进一步处理,直到它到达流的末端为止。模块中的实际处理由任务来完成。每个模块通常有两个任务,一个用于处理到来的消息,一个用于处理外出的消息。在构造协议栈时,这种结构是非常有用的。因为每个模块都有固定的简单接口,所创建的模块可以很容易地在不同的应用间复用。例如,设想一个应用,它处理来自数据链路层的消息。程序员会构造若干模块,每个模块分别处理不同层次的协议。因而,他会构造一个单独的模块,进行网络层处理;另一个进行传输层处理;还有一个进行表示层处理。在构造这些模块之后,它们可以(在ASX的帮助下)被“串”成一个流来使用。如果后来创建了一个新的(也许是更好的)传输模块,就可以在不对程序产生任何影响的情况下、在流中替换先前的传输模块。注意模块就像是容纳任务的容器。这些任务是实际的处理元件。一个模块可能需要两个任务,如同在上面的例子中;也可能只需要一个任务。如你可能会猜到的,ACE_Task是模块中被称为任务的处理元件的实现。

7.3.1.2任务通信的体系结构

  每个ACE_Task都有一个内部的消息队列,用以与其他任务、模块或是外部世界通信。如果一个ACE_Task想要发送一条消息给另一个任务,它就将此消息放入目的任务的消息队列中。一旦目的任务收到此消息,它就会立即对它进行处理。

所有ACE_Task都可以作为0个或多个线程来运行。消息可以由多个线程放入ACE_Task的消息队列,或是从中取出,程序员无需担心破坏任何数据结构。因而任务可被用作由多个协作线程组成的系统的基础构建组件。各个线程控制都可封装在ACE_Task中,与其他任务通过发送消息到它们的消息队列来进行交互。

这种体系结构的唯一问题是,任务只能通过消息队列与在同一进程内的其他任务相互通信。ACE_Svc_Handler解决了这一问题,它同时继承自ACE_Task和ACE_Event_Handler,并且增加了一个私有数据流。这种结合使得ACE_Svc_Handler对象能够用作这样的任务(并发,同一进程);它能够处理事件(异步,不同进程)、并与远地主机的任务间发送和接收数据。

ACE_Task被实现为模板容器,它通过锁定机制来进行实例化。该锁用于保证内部的消息队列在多线程环境中的完整性。如先前所提到的,ACE_Svc_Handler模板容器不仅需要锁定机制,还需要用于与远地任务通信的底层数据流来作为参数。

7.3.1.3 创建ACE_Svc_Handler

  ACE_Svc_Handler模板通过锁定机制和底层流来实例化,以创建所需的服务处理器。如果应用只是单线程的,就不需要使用锁,可以用ACE_NULL_SYNCH来将其实例化。但是,如果我们想要在多线程应用中使用这个模板,可以这样来进行实例化:

class MySvcHandler:

public ACE_Svc_Handler<ACE_SOCK_STREAM,ACE_MT_SYNCH>

{

}

7.3.1.4 在服务处理器中创建多个线程

  在上面的例7-5中,我们使用ACE_Thread包装类和它的静态方法spawn(),创建了单独的线程来发送数据给远地对端。但是,在我们完成此工作时,我们必须定义使用C++ static修饰符的文件范围内的静态send_data()方法。结果当然就是,我们无法访问我们实例化的实际对象的任何数据成员。换句话说,我们被迫使send_data()成员函数成为class-wide的函数,而这并不是我们所想要的。这样做的唯一原因是,ACE_Thread::spawn()只能使用静态成员函数来作为它所创建的线程的入口。另一个有害的副作用是到对端流的引用也必须成为静态的。简而言之,这不是编写这些代码的最好方式。

  ACE_Task提供了更好的机制来避免发生这样的问题。每个ACE_Task都有activate()方法,可用于为ACE_Task创建线程。所创建的线程的入口是非静态成员函数svc()。因为svc()是非静态函数,它可以调用任何对象实例专有的数据或成员函数。ACE对程序员隐藏了该机制的所有实现细节。activate()方法有着非常多的用途,它允许程序员创建多个线程,所有这些线程都使用svc()方法作为它们的入口。还可以设置线程优先级、句柄、名字,等等。activate()方法的原型是: 

// = Active object activation method.

virtual int activate (long flags = THR_NEW_LWP,

int n_threads = 1,

int force_active = 0,

long priority = ACE_DEFAULT_THREAD_PRIORITY,

int grp_id = -1,

ACE_Task_Base *task = 0,

ACE_hthread_t thread_handles[] = 0,

void *stack[] = 0,

size_t stack_size[] = 0,

ACE_thread_t thread_names[] = 0);

  第一个参数flags描述将要创建的线程所希望具有的属性。在线程一章里有详细描述。可用的标志有:

THR_CANCEL_DISABLE, THR_CANCEL_ENABLE, THR_CANCEL_DEFERRED,

THR_CANCEL_ASYNCHRONOUS, THR_BOUND, THR_NEW_LWP, THR_DETACHED,

THR_SUSPENDED, THR_DAEMON, THR_JOINABLE, THR_SCHED_FIFO,

THR_SCHED_RR, THR_SCHED_DEFAULT

  第二个参数n_threads指定要创建的线程的数目。第三个参数force_active用于指定是否应该创建新线程,即使activate()方法已在先前被调用过、因而任务或服务处理器已经在运行多个线程。如果此参数被设为false(0),且如果activate()是再次被调用,该方法就会设置失败代码,而不会生成更多的线程。

  第四个参数用于设置运行线程的优先级。缺省情况下,或优先级被设为ACE_DEFAULT_THREAD_PRIORITY,方法会使用给定的调度策略(在flags中指定,例如,THR_SCHED_DEFAULT)的“适当”优先级。这个值是动态计算的,并且是在给定策略的最低和最高优先级之间。如果显式地给定一个值,这个值就会被使用。注意实际的优先级值极大地依赖于实现,最好不要直接使用。在线程一章中,可读到更多有关线程优先级的内容。

  还可以传入将要创建的线程的线程句柄、线程名和栈空间,以在线程创建过程中使用。如果它们被设置为NULL,它们就不会被使用。但是如果要使用activate创建多个线程,就必须传入线程的名字或句柄,才能有效地对它们进行使用。

7.3.1.5使用服务处理器中的消息队列机制

  如前面所提到的,ACE_Svc_Handler类拥有内建的消息队列。这个消息队列被用作在ACE_Svc_Handler和外部世界之间的主要通信接口。其他任务想要发给该服务处理器的消息被放入它的消息队列中。这些消息会在单独的线程里(通过调用activate()方法创建)处理。随后另一个线程就可以把处理过的消息通过网络发送给另外的远地目的地(很可能是另外的ACE_Svc_Handler)。

  如先前所提到的,在这种多线程情况下,ACE_Svc_Handler会自动地使用锁来确保消息队列的完整性。所用的锁即通过实例化ACE_Svc_Handler模板类创建具体服务处理器时所传递的锁。之所用通过这样的方式来传递锁,是因为这样程序员就可以对他的应用进行“调谐”。不同平台上的不同锁定机制有着不同程度的开销。如果需要,程序员可以创建他自己的优化的、遵从ACE的锁接口定义的锁,并将其用于服务处理器。这是程序员通过使用ACE可获得的灵活性的又一范例。重要的是程序员必须意识到,在此服务处理例程中的额外线程将带来显著的锁定开销。为将此开销降至最低,程序员必须仔细地设计他的程序,确保使这样的开销最小化。特别地,上面描述的例子有可能导致过度的开销,在大多数情况下可能并不实用。

  ACE_Task,进而是ACE_Svc_Handler(因为服务处理器也是一种任务),具有若干可用于对底层队列进行设置、操作、入队和出队操作的方法。这里我们将只讨论这些方法中的一部分。因为在服务处理器中(通过使用msg_queue()方法)可以获取指向消息队列自身的指针,所以也可以直接调用底层队列(也就是,ACE_Message_Queue)的所有公共方法。(有关消息队列提供的所有方法的更多细节,请参见后面的“消息队列”一章。)

  如上面所提到的,服务处理器的底层消息队列是ACE_Message_Queue的实例,它是由服务处理器自动创建的。在大多数情况下,没有必要调用ACE_Message_Queue的底层方法,因为在ACE_Svc_Handler类中已对它们的大多数进行了包装。ACE_Message_Queue是用于使ACE_Message_Block进队或出队的队列。每个ACE_Message_Block都含有指向“引用计数”(reference-counted)的ACE_Data_Block的指针,ACE_Data_Block依次又指向存储在块中的实际数据(见“消息队列”一章)。这使得ACE_Message_Block可以很容易地进行数据共享。

  ACE_Message_Block的主要作用是进行高效数据操作,而不带来许多拷贝开销。每个消息块都有一个读指针和写指针。无论何时我们从块中读取时,读指针会在数据块中向前增长。类似地,当我们向块中写的时候,写指针也会向前移动,这很像在流类型系统中的情况。可以通过ACE_Message_Block的构造器向它传递分配器,以用于分配内存(有关Allocator的更多信息,参见“内存管理”一章)。例如,可以使用ACE_Cached_Allocation_Strategy,它预先分配内存并从内存池中返回指针,而不是在需要的时候才从堆中分配内存。这样的功能在需要可预测的性能时十分有用,比如在实时系统中。

7.4 接受器和连接器模式工作原理

  接受器和连接器工厂(也就是ACE_Connector和ACE_Acceptor)有着非常类似的运行结构。它们的工作可大致划分为三个阶段:

· 端点或连接初始化阶段

· 服务初始化阶段

· 服务处理阶段

7.4.1 端点或连接初始化阶段

  在使用接受器的情况下,应用级程序员可以调用ACE_Acceptor工厂的open()方法,或是它的缺省构造器(它实际上会调用open()方法),来开始被动侦听连接。当接受器工厂的open()方法被调用时,如果反应堆单体还没有被实例化,open()方法就首先对其进行实例化。随后它调用底层具体接受器的open()方法。于是具体接受器会完成必要的初始化来侦听连接。例如,在使用ACE_SOCK_Acceptor的情况中,它打开socket,将其绑定到用户想要在其上侦听新连接的端口和地址上。在绑定端口后,它将会发出侦听调用。open方法随后将接受器工厂登记到反应堆。因而在接收到任何到来的连接请求时,反应堆会自动回调接受器工厂的handle_input()方法。注意正是因为这一原因,接受器工厂才从ACE_Event_Handler类层次派生;这样它才可以响应ACCEPT事件,并被反应堆自动回调。  

  在使用连接器的情况中,应用程序员调用连接器工厂的connect()方法或connect_n()方法来发起到对端的连接。除了其他一些选项,这两个方法的参数包括我们想要连接到的远地地址,以及我们是想要同步还是异步地完成连接。我们可以同步或异步地发起NUMBER_CONN个连接:

//Synchronous

OurConnector.connect_n(NUMBER_CONN,ArrayofMySvcHandlers,Remote_Addr,0,

ACE_Synch_Options::synch);

//Asynchronous

OurConnector.connect_n(NUMBER_CONN,ArrayofMySvcHandlers,Remote_Addr,0,

ACE_Synch_Options::asynch);

  如果连接请求是异步的,ACE_Connector会在反应堆上登记自己,等待连接被建立(ACE_Connector也派生自ACE_Event_Handler类层次)。一旦连接被建立,反应堆将随即自动回调连接器。但如果连接请求是同步的,connect()调用将会阻塞,直到连接被建立、或是超时到期为止。超时值可通过改变特定的ACE_Synch_Options来指定。详情请参见参考手册。

7.4.2 接受器的服务初始化阶段

  在有连接请求在指定的地址和端口上到来时,反应堆自动回调ACE_Acceptor工厂的handle_input()方法。

  该方法是一个“模板方法”(Template Method)。模板方法用于定义一个算法的若干步骤的顺序,并允许改变特定步骤的执行。这种变动是通过允许子类定义这些方法的实现来完成的。(有关模板方法的更多信息见“设计模式”参考指南)。

  在我们的这个案例中,模板方法将算法定义如下:

· make_svc_handler():创建服务处理器。

· accept_svc_handler():将连接接受进前一步骤创建的服务处理器。

· activate_svc_handler():启动这个新服务处理器。

  这些方法都可以被重新编写,从而灵活地决定这些操作怎样来实际执行。

  这样,handle_input()将首先调用make_svc_handler()方法,创建适当类型的服务处理器(如我们在上面的例子中所看到的那样,服务处理器的类型由应用程序员在ACE_Acceptor模板被实例化时传入)。在缺省情况下,make_svc_handler()方法只是实例化恰当的服务处理器。但是,make_svc_handler()是一个“桥接”(bridge)方法,可被重载以提供更多复杂功能。(桥接是一种设计模式,它使类层次的接口与实现去耦合。参阅“设计模式”参考文献)。例如,服务处理器可创建为进程级或线程级的单体,或者从库中动态链接,从磁盘中加载,甚或通过更复杂的方式创建,如从数据库中查找并获取服务处理器,并将它装入内存。

  在服务处理器被创建后,handle_input()方法调用accept_svc_handler()。该方法将连接“接受进”服务处理器;缺省方式是调用底层具体接受器的accept()方法。在ACE_SOCK_Acceptor被用作具体接受器的情况下,它调用BSD accept()例程来建立连接(“接受”连接)。在连接建立后,连接句柄在服务处理器中被自动设置(接受“进”服务处理器);这个服务处理器是先前通过调用make_svc_handler()创建的。该方法也可被重载,以提供更复杂的功能。例如,不是实际创建新连接,而是“回收利用”旧连接。在我们演示各种不同的接受和连接策略时,将更为详尽地讨论这一点。

7.4.3 连接器的服务初始化阶段

  应用发出的connect()方法与接受器工厂中的handle_input()相类似,也就是,它是一个“模板方法”。

  在我们的这个案例中,模板方法connect()定义下面一些可被重定义的步骤:

· make_svc_handler():创建服务处理器。

· connect_svc_handler():将连接接受进前一步骤创建的服务处理器。

· activate_svc_handler():启动这个新服务处理器。

  每一方法都可以被重新编写,从而灵活地决定这些操作怎样来实际执行。

  这样,在应用发出connect()调用后,连接器工厂通过调用make_svc_handler()来实例化恰当的服务处理器,一如在接受器的案例中所做的那样。其缺省行为只是实例化适当的类,并且也可以通过与接受器完全相同的方式重载。进行这样的重载的原因可以与上面提到的原因非常类似。

  在服务处理器被创建后,connect()调用确定连接是要成为异步的还是同步的。如果是异步的,在继续下一步骤之前,它将自己登记到反应堆,随后调用connect_svc_handler()方法。该方法的缺省行为是调用底层具体连接器的connect()方法。在使用ACE_SOCK_Connector的情况下,这意味着将适当的选项设置为阻塞或非阻塞式I/O,然后发出BSD connect()调用。如果连接被指定为同步的,connect()调用将会阻塞、直到连接完全建立。在这种情况下,在连接建立后,它将在服务处理器中设置句柄,以与它现在连接到的对端通信(该句柄即是通过在服务处理器中调用peer()方法获得的在流中存储的句柄,见上面的例子)。在服务处理器中设置句柄后,连接器模式将进行到最后阶段:服务处理。

  如果连接被指定为异步的,在向底层的具体连接器发出非阻塞式connect()调用后,对connect_svc_handler()的调用将立即返回。在使用ACE_SOCK_Connector的情况中,这意味着发出非阻塞式BSD connect()调用。在连接稍后被实际建立时,反应堆将回调ACE_Connector工厂的handle_output()方法,该方法在通过make_svc_handler()方法创建的服务处理器中设置新句柄。然后工厂将进行到下一阶段:服务处理。

  与accept_svc_handler()情况一样,connect_svc_handler()是一个“桥接”方法,可进行重载以提供变化的功能。

7.4.4 服务处理(是不是有问题???)

  一旦服务处理器被创建、连接被建立,以及句柄在服务处理器中被设置,ACE_Acceptor的handle_input()方法(或者在使用ACE_Connector的情况下,是handle_output()或connect_svc_handler())将调用activate_svc_handler()方法。该方法将随即启用服务处理器。其缺省行为是调用作为服务处理器的入口的open()方法。如我们在上面的例子中所看到的,在服务处理器开始执行时,open()方法是第一个被调用的方法。是在open()方法中,我们调用activate()方法来创建多个线程控制;并在反应堆上登记服务处理器,这样当新的数据在连接上到达时,它会被自动回调。该方法也是一个“桥接”方法,可被重载以提供更为复杂的功能。特别地,这个重载的方法可以提供更为复杂的并发策略,比如,在另一不同的进程中运行服务处理器。

7.5 调谐接受器和连接器策略

  如上面所提到的,因为使用了可以重载的桥接方法,很容易对接受器和连接器进行调谐。桥接方法允许调谐:

· 服务处理器的创建策略:通过重载接受器或连接器的make_svc_handler()方法来实现。例如,这可以意味着复用已有的服务处理器,或使用某种复杂的方法来获取服务处理器,如上面所讨论的那样。

· 连接策略:连接创建策略可通过重载connect_svc_handler()或accept_svc_handler()方法来改变。

· 服务处理器的并发策略:服务处理器的并发策略可通过重载activate_svc_handler()方法来改变。例如,服务处理器可以在另外的进程中创建。

  如上所示,调谐是通过重载ACE_Acceptor或ACE_Connector类的桥接方法来完成的。ACE的设计使得程序员很容易完成这样的重载和调谐。

7.5.1 ACE_Strategy_Connector和ACE_Strategy_Acceptor类

  为了方便上面所提到的对接受器和连接器模式的调谐方法,ACE提供了两种特殊的“可调谐”接受器和连接器工厂,那就是ACE_Strategy_Acceptor和ACE_Strategy_Connector。它们和ACE_Acceptor与ACE_Connector非常类似,同时还使用了“策略”模式。

  策略模式被用于使算法行为与类的接口去耦合。其基本概念是允许一个类(称为Context Class,上下文类)的底层算法独立于使用该类的客户进行变动。这是通过具体策略类的帮助来完成的。具体策略类封装执行操作的算法或方法。这些具体策略类随后被上下文类用于执行各种操作(上下文类将“工作”委托给具体策略类)。因为上下文类不直接执行任何操作,当需要改变功能时,无需对它进行修改。对上下文类所做的唯一修改是使用另一个具体策略类来执行改变了的操作。(要阅读有关策略模式的更多信息,参见“设计模式”的附录)。

  在ACE中,ACE_Strategy_Connector和ACE_Strategy_Acceptor使用若干具体策略类来改变算法,以创建服务处理器,建立连接,以及为服务处理器设置并发方法。如你可能已经猜到的一样,ACE_Strategy_Connector和ACE_Strategy_Acceptor利用了上面提到的桥接方法所提供的可调谐性。

7.5.1.1 使用策略接受器和连接器

  在ACE中已有若干具体的策略类可用于“调谐”策略接受器和连接器。当类被实例化时,它们作为参数被传入策略接受器或连接器。表7-2显示了可用于调谐策略接受器和连接器类的一些类。

需要修改

具体策略类

描述

创建策略

(重定义make_svc_handler())

ACE_NOOP_Creation_Strategy

这个具体策略并不实例化服务处理器,而只是一个空操作。

ACE_Singleton_Strategy

保证服务处理器被创建为单体。也就是,所有连接将有效地使用同一个服务处理例程。

ACE_DLL_Strategy

通过从动态链接库中动态链接服务处理器来对它进行创建。

连接策略

(重定义connect_svc_handler())

ACE_Cached_Connect_Strategy

检查是否有已经连接到特定的远地地址的服务处理器没有在被使用。如果有这样一个服务处理器,就对它进行复用。

并发策略

(重定义activate_svc_handler())

ACE_NOOP_Concurrency_Strategy

一个“无为”(do-nothing)的并发策略。它甚至不调用服务处理器的open()方法。

ACE_Process_Strategy

在另外的进程中创建服务处理器,并调用它的open()方法。

ACE_Reactive_Strategy

先在反应堆上登记服务处理器,然后调用它的open()方法。

ACE_Thread_Strategy

先调用服务处理器的open()方法,然后调用它的activate()方法,以让另外的线程来启动服务处理器的svc()方法。

         表7-2 用于调谐策略接受器和连接器类的类

7.5.1.2 使用ACE_Cached_Connect_Strategy进行连接缓存

  在许多应用中,客户会连接到服务器,然后重新连接到同一服务器若干次;每次都要建立连接,执行某些工作,然后挂断连接(比如像在Web客户中所做的那样)。不用说,这样做是非常低效而昂贵的,因为连接建立和挂断是非常昂贵的操作。在这样的情况下,连接者可以采用一种更好的策略:“记住”老连接并保持它,直到确定客户不会再重新建立连接为止。ACE_Cached_Connect_Strategy就提供了这样一种缓存策略。这个策略对象被ACE_Strategy_Connector用于提供基于缓存的连接建立。如果一个连接已经存在,ACE_Strategy_Connector将会复用它,而不是创建新的连接。

  当客户试图重新建立连接到先前已经连接的服务器时,ACE_Cached_Connect_Strategy确保对老的连接和服务处理器进行复用,而不是创建新的连接和服务处理器。因而,实际上,ACE_Cached_Connect_Strategy不仅管理连接建立策略,它还管理服务处理器创建策略。因为在此例中,用户不想创建新的服务处理器,我们将ACE_Null_Creation_Strategy传递给ACE_Strategy_Connector。如果连接先前没有建立过,ACE_Cached_Connect_Strategy将自动使用内部的创建策略来实例化适当的服务处理器,它是在这个模板类被实例化时传入的。这个策略可被设置为用户想要使用的任何一种策略。除此而外,也可以将ACE_Cached_Connect_Strategy自己在其构造器中使用的创建、并发和recycling策略传给它。

第8章 服务配置器(Service Configurator):用于服务动态配置的模式

  如果服务可以被动态地启动、移除、挂起和恢复,那将会方便得多。这样,服务开发者就不必再担心配置的服务。他所需关心的是服务如何完成工作。管理员就可以在应用中增加或替换新服务,而不用重新编译或关闭服务进程。

  服务配置器模式可以完成所有这些任务。它使服务的实现与配置去耦合。无需关闭服务器,就可以在应用中增加新服务和移除旧服务。在大多数情况下,提供服务的服务器都被实现为看守(daemon)进程。

8.1 构架组件

  ACE中的服务配置器由以下组件组成:

· 名为ACE_Service_Object的抽象类。应用开发者必须从它派生出子类,以创建他自己的应用特有的具体服务对象(Service Object)。

· 应用特有的具体服务对象。

· 服务仓库ACE_Service_Repository。它记录服务器所运行的和所知道的服务。

· ACE_Service_Config。它是整个服务配置器框架的应用开发接口。

· 服务配置文件。该文件含有所有服务对象的配置信息。其缺省的名字是svc.conf。当你的应用对ACE_Service_Config发出open()调用时,服务配置器框架会读取并处理你写在此文件中的所有配置信息,随后相应地配置应用。

  ACE_Service_Object包括了一些由框架调用的方法,用于服务要启动(init())、停止(fini())、挂起(suspend())或是恢复(resume())时。ACE_Service_Object派生自ACE_Shared_Object和ACE_Event_Handler。ACE_Shared_Object在应用想要使用操作系统的动态链接机制来进行加载时被用作抽象基类。ACE_Event_Handler已在对反应堆的讨论中进行了介绍。当开发者想要他的类响应来自反应堆的事件时,他就从ACE_Event_Handler派生他的子类。

  为什么服务对象要从ACE_Event_Handler继承?用户发起重配置的一种方法是生成一个信号;当这样的信号事件发生时,反应堆被用于处理信号,并向ACE_Service_Config发出重配置请求。除此而外,软件的重配置也可能在某事件产生后发生。因而所有的服务对象都被构造为能对事件进行处理。

  服务配置文件有它自己的简单脚本,用于描述你想要服务怎样启动和运行。你可以定义你是想要增加新服务,还是挂起、恢复或移除应用中现有的服务。另外还可以给服务发送参数。服务配置器还允许进行基于ACE的流(stream)的重配置。我们将在讨论了ACE流构架之后再来更多地讨论这一点。

8.2 定义配置文件

  服务配置文件指定在应用中哪些服务要被加载和启动。此外,你可以指定哪些服务要被停止、挂起或恢复。还可以发送参数给你的服务对象的init()方法。

8.2.1 启动服务

  服务可以被静态或动态地启动。如果服务要动态启动,服务配置器实际上会从共享对象库(也就是,动态链接库)中加载服务对象。为此,服务配置器需要知道哪个库含有此对象,并且还需要知道对象在该库中的名字。因而,在你的代码文件中你必须通过你需要记住的名字来实例化服务对象。于是动态服务会这样被配置:

dynamic service_name type_of_service * location_of_shared_lib:name_of_object “parameters”

而静态服务这样被初始化:

static service_name “parameters_send_to_service_object”

8.2.2 挂起或恢复服务

  如刚才所提到的,你在启动服务时分配给它一个名字。这个名字随后被用于挂起或恢复该服务。于是要挂起服务,你所要做的就是在svc.conf文件中指定:

  suspend service_name

  这使得服务对象中的suspend()方法被调用。随后你的服务对象就应该挂起它自己(基于特定服务不同的“挂起”含义)。

  如果你想要恢复这个服务,你所要做的就是在svc.conf文件中指定:

  resume service_name

  这使得服务对象中的resume()方法被调用。随后你的服务对象就应该恢复它自己(基于特定服务不同的“恢复”含义。)

8.2.3 停止服务

  停止并移除服务(如果服务是动态加载的)同样是很简单的操作,可以通过在你的配置文件中指定以下指令来完成:

remove service_name

  这使得服务配置器调用你的应用的fini()方法。该方法应该使此服务停止。服务配置器自己会负责将动态对象从服务器的地址空间里解除链接。

8.3 编写服务

  为服务配置器编写你自己的服务相对比较简单。你可以让这个服务做任何你想做的事情。唯一的约束是它应该是ACE_Service_Object的子类。所以它必须实现init()和fini()方法。在ACE_Service_Config被打开(open())时,它读取配置文件(也就是svc.conf)并根据这个文件来对服务进行初始化。一旦服务被加载,它会调用该服务对象的init()方法。类似地,如果配置文件要求移除服务,fini()方法就会被调用。这些方法负责分配和销毁服务所需的任何资源,比如内存、连接、线程等等。在svc.conf文件中指定的参数通过服务对象的init()方法来传入。

  下面的例子演示一个派生自ACE_Task_Base的服务。ACE_Task_Base类含有activate()方法,用于在对象里创建线程。(在“任务和主动对象”一章中讨论过的ACE_Task派生自ACE_Task_Base,并包括了用于通信目的的消息队列。因为我们不需要我们的服务与其它任务通信,我们仅仅使用ACE_Task_Base来帮助我们完成工作。)更多详细信息,请阅读“任务和主动对象”一章。该服务是一个“无为”(do-nothing)的服务,一旦启动,它只是周期性地广播当天的时间。

  相应的实现如下所述:在时间服务接收到init()调用时,它在任务中启用(activate())一个线程。这将会创建一个新线程,其入口为svc()方法。在svc()方法中,该线程将会进行循环,直到它看到canceled_标志被设置为止。此标志在服务配置构架调用fini()时设置。但是,在fini()方法返回底层的服务配置框架之前,它必须确定在底层的线程已经终止。因为服务配置器将要实际地卸载含有TimeService的共享库,从而将TimeService对象从应用进程中删除。如果在此之前线程并未终止,它将会对已经被服务配置器“蒸发”的代码发出调用!我们当然不需要这个。为了确保线程在服务配置器“蒸发”TimeService对象之前终止,程序使用了条件变量。(要更多地了解怎样使用条件变量,请阅读有关线程的章节)。

  下面是一个简单的、只是用于启用时间服务的配置文件。可以去掉注释#号来挂起、恢复和移除服务。

例8-1c

# To configure different services, simply uncomment the appropriate

#lines in this file!

#resume TimeService

#suspend TimeService

#remove TimeService

#set to dynamically configure the TimeService object and do so without

#sending any parameters to its init method

dynamic TimeService Service_Object * ./Server:time_service ""

  最后,下面是启动服务配置器的代码段。这些代码还设置了一个信号处理器对象,用于发起重配置。该信号处理器已被设置成响应SIGWINCH信号(在窗口发生变化时产生的信号)。在启动服务配置器之后,应用进入一个反应式循环,等待SIGWINCH信号事件发生。一旦事件发生,就会回调事件处理器,由它调用ACE_Service_Config的reconfigure()方法。如先前所讲述的,在此调用发生时,服务配置器重新读取配置文件,并处理用户放在其中的任何新指令。例如,在动态启动TimeService后,在这个例子中你可以改变svc.conf文件,只留下一个挂起命令在里面。当配置器读取它时,它将调用TimeService的挂起方法,从而使它挂起它的底层线程。类似地,如果稍后你又改变了svc.conf,要求恢复服务,配置器就会调用TimeService::resume()方法,从而恢复先前被挂起的线程。

8.4 使用服务管理器

  ACE_Service_Manager是可用于对服务配置器进行远程管理的服务。它目前可以接受两种类型的请求。其一,你可以向它发送“help”消息,列出当前被加载进应用的所有服务。其二,你可以向服务管理器发送“reconfigure”消息,从而使得服务配置器重新配置它自己。

第9章 消息队列(Message Queue)

  现代的实时应用通常被构建成一组相互通信、但又相互独立的任务。这些任务可以通过若干机制来与对方进行通信,其中常用的一种就是消息队列。在这一情况下,基本的通信模式是:发送者(或生产者)任务将消息放入消息队列,而接收者(或消费者)任务从此队列中取出消息。这当然只是消息队列的使用方式之一。在接下来的讨论中,我们将看到若干不同的使用消息队列的例子。

  ACE中的消息队列是仿照UNIX系统V的消息队列设计的,如果你已经熟悉系统V的话,就很容易掌握ACE的消息队列的使用。在ACE中有多种不同类型的消息队列可用,每一种都使用不同的调度算法来进行队列的入队和出队操作。 

9.1 消息块

  在ACE中,消息作为消息块(Message Block)被放入消息队列中。消息块包装正被存储的实际消息数据,并提供若干数据插入和处理操作。每个消息块“包含”一个头和一个数据块。注意在这里“包含”是在宽松的意义上使用的。消息块可以不对与数据块(Data Block)或是消息头(Message Header)相关联的内存进行管理(尽管你可以让消息块进行这样的管理)。它仅仅持有指向两者的指针。所以包含只是逻辑上的。数据块持有指向实际的数据缓冲区的指针。如图9-1所示,这样的设计带来了多个消息块之间的数据的灵活共享。注意在图中两个消息块共享一个数据块。这样,无需带来数据拷贝开销,就可以将同一数据放入不同的队列中。

  消息块类名为ACE_Message_Block,而数据块类名为ACE_Data_Block。ACE_Message_Block的构造器是实际创建消息块和数据块的方便办法。

9.1.1 构造消息块

  ACE_Message_Block类包含有若干不同的构造器。你可以使用这些构造器来帮助你管理隐藏在消息和数据块后面的消息数据。ACE_Message_Block类可用于完全地隐藏ACE_Data_Block,并为你管理消息数据;或者,如果你需要,你可以自己创建和管理数据块及消息数据。下一部分将考查怎样使用ACE_Message_Block来管理消息内存和数据块。然后我们将考查怎样独立地进行这样的管理,而不用依赖ACE_Message_Block的管理特性。

9.1.1.1 ACE_Message_Block分配和管理数据内存

  要创建消息块,最容易的方法是将底层数据块的大小传给ACE_Message_Block的构造器,从而创建ACE_Data_Block,并为消息数据分配空的内存区。在创建消息块后,你可以使用rd_ptr()和wr_ptr()方法来在消息块中插入和移除数据。让ACE_Message_Block来为数据和数据块创建内存区的主要优点是,它会为你正确地管理所有内存,从而使你免于在将来为许多内存泄漏而头疼。

  ACE_Message_Block的构造器还允许程序员指定ACE_Message_Block在内部分配内存时所应使用的分配器。如果你传入一个分配器,消息块将用它来为数据块和消息数据区的创建分配内存。ACE_Message_Block的构造器为:

ACE_Message_Block (size_t size,

ACE_Message_Type type = MB_DATA,

ACE_Message_Block *cont = 0,

const char *data = 0,

ACE_Allocator *allocator_strategy = 0,

ACE_Lock *locking_strategy = 0,

u_long priority = 0,

const ACE_Time_Value & execution_time = ACE_Time_Value::zero,

const ACE_Time_Value & deadline_time = ACE_Time_Value::max_time);

  上面的构造器的参数为:

1. 要与消息块相关联的数据缓冲区的大小。注意消息块的大小是size,但长度将为0,直到wr_ptr被设置为止。这将在后面进一步解释。

2. 消息的类型。(在ACE_Message_Type枚举中有若干类型可用,其中包括缺省的数据消息)。

3. 指向“片段链”(fragment chain)中的下一个消息块的指针。消息块可以实际地链接在一起来形成链。随后链可被放入消息队列中,就好像它是单个数据块一样。该参数缺省为0,意味着此块不使用链。

4. 指向要存储在此消息块中的数据缓冲区的指针。如果该参数的值为零,就会创建缓冲区(大小由第一个参数指定),并由该消息块进行管理。当消息块被删除时,相应的数据缓冲区也被删除。但是,如果在此参数中指定了数据缓冲区,也就是,参数不为空,当消息块被销毁时它就不会删除数据缓冲区。这是一个重要特性,必须牢牢记住。

5. 用于分配数据缓存(如果需要)的allocator_strategy,在第四个参数为空时使用(如上面所解释的)。任何ACE_Allocator的子类都可被用作这一参数。(关于ACE_Allocator的更多信息,参见“内存管理”一章)。

6. 如果locking_strategy不为零,它就将用于保护访问共享状态(例如,引用计数)的代码区,以避免竞争状态。

7. 这个参数以及后面两个参数用于ACE中的实时消息队列的调度,目前应保留它们的缺省值。

9.1.1.2 用户分配和管理消息内存

  如果你正在使用ACE_Message_Block,你并不一定要让它来为你分配内存。消息块的构造器允许你: 

· 创建并传入你自己的指向消息数据的数据块。

· 传入指向消息数据的指针,消息块将创建并设置底层的数据块。消息块将为数据块、而不是消息数据管理内存。

  下面的例子演示怎样将指向消息数据的指针传给消息块,以及ACE_Message_Block怎样创建和管理底层的ACE_Data_Block。

//The data

char data[size];

data = ”This is my data”;

//Create a message block to hold the data

ACE_Message_Block *mb = new ACE_Message_Block (data, // data that is stored

// in the newly created data

//

blocksize); //size of the block that

//is to be stored.

  该构造器创建底层数据块,并将它设置为指向传递给它的数据的开头。被创建的消息块并不拷贝该数据,也不假定自己拥有它的所有权。这就意味着在消息块mb被销毁时,相关联的数据缓冲区data将不会被销毁。这是有意义的:消息块没有拷贝数据,因此内存也不是它分配的,这样它也不应该负责销毁它。

9.1.2 在消息块中插入和操作数据

  除了构造器,ACE_Message_Block还提供若干方法来直接在消息块中插入数据。另外还有一些方法可用来操作已经在消息块中的数据。

  每个ACE_Message_Block都有两个底层指针:rd_ptr和wr_ptr,用于在消息块中读写数据。它们可以通过调用rd_ptr()和wr_ptr()方法来直接访问。rd_ptr指向下一次读取数据的位置,而wr_ptr指向下一次写入数据的位置。程序员必须小心地管理这些指针,以保证它们总是指向正确的位置。在使用这些指针读写数据时,程序员必须自己来增加它们的值,它们不会魔法般地自动更新。大多数内部的消息块方法也使用这两个指针,从而使它们能够在你调用消息块方法时改变指针状态。程序员必须保证自己了解这些指针的变化。

9.1.2.1 拷贝与复制(Copying and Duplicating)

  可以使用ACE_Message_Block的copy()方法来将数据拷贝进消息块。

int copy(const char *buf, size_t n);

  copy方法需要两个参数,其一是指向要拷贝进消息块的缓冲区的指针,其二是要拷贝的数据的大小。该方法从wr_pt指向的位置开始往前写,直到它到达参数n所指示的数据缓冲区的末尾。copy()还会保证wr_ptr的更新,使其指向缓冲区的新末尾处。注意该方法将实际地执行物理拷贝,因而应该小心使用。

  base()和length()方法可以联合使用,以将消息块中的整个数据缓冲区拷贝出来。base()返回指向数据块的第一个数据条目的指针,而length()返回队中数据的总大小。将base和length相加,可以得到指向数据块末尾的指针。合起来使用这些方法,你就可以写一个例程来从消息块中取得数据,并做一次外部拷贝。

  duplicate()和clone()方法用于制作消息块的“副本”。如它的名字所暗示的,clone()方法实际地创建整个消息块的新副本,包括它的数据块和附加部分;也就是说,这是一次“深度复制”。而另一方面,duplicate()方法使用的是ACE_Message_Block的引用计数机制。它返回指向要被复制的消息块的指针,并在内部增加内部引用计数。

9.1.2.2 释放消息块

  一旦使用完消息块,程序员可以调用它的release()方法来释放它。如果消息数据内存是由该消息块分配的,调用release()方法就也会释放此内存。如果消息块是引用计数的,release()就会减少计数,直到到达0为止;之后消息块和与它相关联的数据块才从内存中被移除。

9.2 ACE的消息队列

  如先前所提到的,ACE有若干不同类型的消息队列,它们大体上可划分为两种范畴:静态的和动态的。静态队列是一种通用的消息队列(ACE_Message_Queue),而动态消息队列(ACE_Dynamic_Message_Queue)是实时消息队列。这两种消息队列的主要区别是:静态队列中的消息具有静态的优先级,也就是,一旦优先级被设定就不会再改变;而另一方面,在动态消息队列中,基于诸如执行时间和最终期限等参数,消息的优先级可以动态地改变。

例子由一个Qtest类组成,它通过ACE_NULL_SYNCH锁定来实例化缺省大小的消息队列。锁(互斥体和条件变量)被消息队列用来: 

· 保证由消息块维护的引用计数的安全,防止在有多个线程访问时的竞争状态。

· “唤醒”所有因为消息队列空或满而休眠的线程。

  在此例中,因为只有一个线程,消息队列的模板同步参数被设置为空(ACE_NULL_SYNCH,意味着使用ACE_Null_Mutex和ACE_Null_Condition)。随后Qtest的enq_msgs()方法被调用,它进入循环,创建消息、并将其放入消息队列中。消息数据的大小作为参数传给ACE_Message_Block的构造器。使用该构造器使得内存被自动地管理(也就是,内存将在消息块被删除时,即release()时被释放)。wr_ptr随后被获取(使用wr_ptr()访问方法),且数据被拷贝进消息块。在此之后,wr_ptr向前增长。然后使用消息队列的enqueue_prio()方法来实际地将消息块放入底层消息队列中。

  在no_msgs_个消息块被创建、初始化和插入消息队列后,enq_msgs()调用deq_msgs()方法。该方法使用ACE_Message_Queue的dequeue_head()方法来使消息队列中的每个消息出队。在消息出队后,就显示它的数据,然后再释放消息。

9.3 水位标

  水位标用于在消息队列中指示何时在其中的数据已过多(消息队列到达了高水位标),或何时在其中的数据的数量不足(消息队列到达了低水位标)。两种水位标都用于流量控制。例如,low_water_mark可用于避免像TCP中的“傻窗口综合症”(silly window syndrome)那样的情况,而high_water_mark可用于“阻止“或减缓数据的发送或生产。

  ACE中的消息队列通过维护已经入队的总数据量的字节计数来获得这些功能。因而,无论何时有新消息块被放入消息队列中,消息队列都将先确定它的长度,然后检查是否能将此消息块放入队列中(也就是,确认如果将此消息块入队,消息队列没有超过它的高水位标)。如果消息队列不能将数据入队,而它又持有一个锁(也就是,使用了ACE_SYNC,而不是ACE_NULL_SYNCH作为消息队列的模板参数),它就会阻塞调用者,直到有足够的空间可用,或是入队方法的超时(timeout)到期。如果超时已到期,或是队列持有一个空锁,入队方法就会返回-1,指示无法将消息入队。

  类似地,当ACE_Message_Queue的dequeue_head方法被调用时,它检查并确认在出队之后,剩下的数据的数量高于低水位标。如果不是这样,而它又持有一个锁,它就会阻塞;否则就返回-1,指示失败(和入队方法的工作方式一样)。

  分别有两个方法可用于设置和获取高低水位标:

//get the high water mark

size_t high_water_mark(void)

//set the high water mark

void high_water_mark(size_t hwm);

//get the low water_mark

size_t low_water_mark(void)

//set the low water_mark

void low_water_mark(size_t lwm)

9.4 使用消息队列迭代器(Message Queue Iterator)

  和其它容器类的常见情况一样,可将前进(forward)和后退(reverse)迭代器用于ACE中的消息队列。这两个迭代器名为ACE_Message_Queue_Iterator和ACE_Message_Queue_Reverse_Iterator。它们都需要一个模板参数,用于在遍历消息队列时进行同步。如果有多个线程使用消息队列,该参数就应设为ACE_SYNCH;否则,就可设为ACE_NULL_SYNCH。在迭代器对象被创建时,必须将我们想要进行迭代的消息队列的引用传给它的构造器。

9.5 动态或实时消息队列

  如上面所提到的,动态消息队列是其中的消息的优先级随时间变化的队列。实时应用需要这样的行为特性,因而这样的队列在实时应用中天生更为有用。

  ACE目前提供两种动态消息队列:基于最终期限(deadline)的和基于松弛度(laxity)的(参见[IX])动态消息队列。基于最终期限的消息队列通过每个消息的最终期限来设置它们的优先级。在使用最早deadline优先算法来调用dequeue_head()方法时,队列中有着最早的最终期限的消息块将最先出队。而基于松弛度的消息队列,同时使用执行时间和最终期限来计算松弛度,并将其用于划分各个消息块的优先级。松弛度是十分有用的,因为在根据最终期限来调度时,被调度的任务有可能有最早的最终期限,但同时又有相当长的执行时间,以致于即使它被立即调度,也不能够完成。这会消极地影响其它任务,因为它可能阻塞那些可以调度的任务。松弛度把这样的长执行时间考虑在内,并保证任务如果不能完成,就不会被调度。松弛度队列中的调度基于最小松弛度优先算法。

  基于松弛度的消息队列和基于最终期限的消息队列都实现为ACE_Dynamic_Message_Queue。ACE使用策略(STRATEGY)模式来为动态队列提供不同的调度特性。每种消息队列使用不同的“策略”对象来动态地设置消息队列中消息的优先级。每个这样的“策略”对象都封装了一种不同的算法来基于执行时间、最终期限,等等,计算优先级;并且无论何时消息入队或是出队,都会调用这些策略对象来完成前述计算工作。(有关策略模式的更多信息,请参见“设计模式”)。消息策略模式派生自ACE_Dynamic_Message_Strategy,目前有两种策略可用:ACE_Laxity_Message_Strategy和ACE_Deadline_Message_Strategy。因此,要创建基于松弛度的动态消息队列,首先必须创建ACE_Laxity_Message_Strategy对象。随后,应该对ACE_Dynamic_Message_Queue对象进行实例化,并将新创建的策略对象作为参数之一传给它的构造器。

创建消息队列

  为简化这些不同类型的消息队列的创建,ACE提供了名为ACE_Message_Queue_Factory的具体消息队列工厂,它使用工厂方法(FACTORY METHOD,更多信息参见“设计模式”)模式的一种变种来创建适当类型的消息队列。消息队列工厂有三个静态的工厂方法,可用来创建三种不同类型的消息队列:

static ACE_Message_Queue<ACE_SYNCH_USE> *

create_static_message_queue();

static ACE_Dynamic_Message_Queue<ACE_SYNCH_USE> *

create_deadline_message_queue();

static ACE_Dynamic_Message_Queue<ACE_SYNCH_USE> *

create_laxity_message_queue();

  每个方法都返回指向刚创建的消息队列的指针。注意这些方法都是静态的,而create_static_message_queue()方法返回的是ACE_Message_Queue,其它两个方法则返回ACE_Dynamic_Message_Queue。

能看到这里对ACE估计感兴趣,下载学习ACE调试过的例程代码

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics