ACE 学习

作者:陈珍敬

ACE 的配置(window )

(使用VC ++)安装:

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

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

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

执行: 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 中 ../../../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 调用中之前,条件变量机制(也就是 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 类派生子类 。 在子类派生之后,必须采取以下步骤:

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 返回的设备句柄 )。网络应用显然是最适于这样的多路分离的应用之一。下面的例子演示反应堆是怎样与具体接受器一起使用来构造一个服务器的。

#include "ace/Reactor.h"

#include "ace/SOCK_Acceptor.h"

#define PORT_NO 1024

typedef ACE_SOCK_Acceptor Acceptor;

//forward declaration

class My_Accept_Handler;

class My_Input_Handler: public ACE_Event_Handler

{

public:

//Constructor

My_Input_Handler()

{

ACE_DEBUG((LM_DEBUG,"Constructor/n"));

}

//Called back to handle any input received

int handle_input(ACE_HANDLE)

{

//receive the data

peer_.recv_n(data,12);

ACE_DEBUG((LM_DEBUG,"%s/n",data));

// do something with the input received.

// ...

//keep yourself registered with the reactor

return 0;

}

//Used by the reactor to determine the underlying handle

ACE_HANDLE get_handle() const

{

return this->peer_.get_handle();

//return this->peer_i().get_handle();

}

//Returns a reference to the underlying stream.

ACE_SOCK_Stream &peer_i()

{

return this->peer_;

}

ACE_SOCK_Stream peer_;  //public

private:

//ACE_SOCK_Stream peer_;

char data [12];

};

class My_Accept_Handler: public ACE_Event_Handler

{

public:

//Constructor

My_Accept_Handler(ACE_Addr &addr)

{

this->open(addr);

}

//Open the peer_acceptor so it starts to ”listen ”

//for incoming clients.

int open(ACE_Addr &addr)

{

peer_acceptor.open(addr);

return 0;

}

//Overload the handle input method

int handle_input(ACE_HANDLE handle)

{

//Client has requested connection to server.

//Create a handler to handle the connection

My_Input_Handler *eh = new My_Input_Handler();

//Accept the connection "into" the Event Handler

if (this->peer_acceptor.accept (eh->peer_, // stream

0, // remote address

0, // timeout

1) ==-1) //restart if interrupted

{

ACE_DEBUG((LM_ERROR,"Error in connection/n"));

}

ACE_DEBUG((LM_DEBUG,"Connection established/n"));

//Register the input event handler for reading

ACE_Reactor::instance()->register_handler(eh,ACE_Event_Handler::READ_MASK);

//Unregister as the acceptor is not expecting new clients

return -1;

}

//Used by the reactor to determine the underlying handle

ACE_HANDLE get_handle(void) const

{

return this->peer_acceptor.get_handle();

}

private:

Acceptor peer_acceptor;

};

int main(int argc, char * argv[])

{

//Create an address on which to receive connections

ACE_INET_Addr addr(PORT_NO);

//Create the Accept Handler which automatically begins to “listen ”

//for client requests for connections

My_Accept_Handler *eh=new My_Accept_Handler(addr);

//Register the reactor to call back when incoming client connects

ACE_Reactor::instance()->register_handler(eh,ACE_Event_Handler::ACCEPT_MASK);

//Start the event loop

while(1)

{

ACE_Reactor::instance()->handle_events();

}

return 0;

}

第一个具体事件处理器 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 组件

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

接受器类型

所用地址

所用流

具体接受器

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

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

 

 

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

  这样, 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() 定义下面一些可被重定义的步骤 :

 

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

  这样,在应用发出 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学习相关推荐

  1. ACE高性能网络编程——学习大纲

    ACE高性能网络编程 一.课程目标 本次课程针对使用ACE工具包进行高性能网络应用开发,通过本次课程的学习,学员将具备以下能力: 了解ACE的架构和组件: 理解现代操作系统线程模型.并发以及同步机制: ...

  2. keras系列︱迁移学习:利用InceptionV3进行fine-tuning及预测、完美案例(五)

    之前在博客<keras系列︱图像多分类训练与利用bottleneck features进行微调(三)>一直在倒腾VGG16的fine-tuning,然后因为其中的Flatten层一直没有真 ...

  3. 斯坦福大学CS520知识图谱系列课程学习笔记:第三讲高级的知识图谱是什么样的

    这一讲的主题是:一些高级的知识图谱是什么样的.三位讲者分别从他们各自带领的团队构建出来的高质量的知识图谱实例出发,从中我们可以认识到一些大佬们构建的优秀的知识图谱产品有哪些特点和优势,从中对我们自己构 ...

  4. 斯坦福大学CS520知识图谱系列课程学习笔记:第二讲如何构建知识图谱

    上一讲我们学习了知识图谱的一些基本概念: 斯坦福大学CS520知识图谱系列课程学习笔记:第一讲什么是知识图谱 本节课程关于如何构建知识图谱,因为知识图谱的构建是整个知识图谱领域的一个非常核心且基础的工 ...

  5. 详细介绍Linux shell脚本系列基础学习(列表)

    本系列适合Linux初学者,属于Linux入门级教程,主要介绍了Shell的分类.语法格式以及脚本的使用和编写格式等. 不断更新中,是Shell学习的必读经典教程.现主要包含以下文章: Linux S ...

  6. python3《机器学习实战系列》学习笔记----3.2 决策树实战

    前言 一.ID3算法构造决策树 1.1 背景 1.2 信息增益计算 1.3 递归生成决策树 二.使用Matplotlib注解绘制树形图 2.1 Matplotlib注解 2.2 构造注解树 三.测试和 ...

  7. mysql入门很简单系列视频-学习笔记

    mysql入门很简单系列视频-学习笔记 视频链接:mysql入门很简单系列视频 https://www.bilibili.com/video/av14920200/ 以前主要就了解DDL.DML.DC ...

  8. 区块链知识系列 - 系统学习EVM(二)-存储与安全

    区块链知识系列 - 系统学习EVM(一) 特点 EVM出于所谓运算速度和效率方面考虑,采用了非主流的256bit整数. 不支持浮点数 缺乏标准库支持,例如字符串拼接.切割.查找等等都需要开发者自己实现 ...

  9. 奋斗的小孩系列 FPGA学习altera系列: FPGA学习altera 系列 第二十一篇 数码管设计

    奋斗的小孩系列 FPGA学习altera系列: FPGA学习altera 系列 第二十一篇 数码管设计 作者:奋斗的小孩 郝旭帅(转载请注明出处) 大侠好,欢迎来到FPGA技术江湖,江湖偌大,相见即是 ...

最新文章

  1. 李开复:发明期已过,AI科学家创业需谨慎(附演讲视频)
  2. simulink中错误object[id]!=NULL Component: Simulink | Category:Model error
  3. todo已完成任务_重要主干街路已完成清雪任务
  4. Hive更换TEZ引擎
  5. 【C++深度剖析教程32】new/malloc区别 delete/free区别
  6. Spring Cloud微服务之网关服务创建(十四)完结篇
  7. 传统emmc所用的sdio接口_SolidGear SD/SDIO/eMMC协议分析仪
  8. 千里之行始于足下---在“从普通走向优秀”的路上
  9. Json学习总结(5)——阿里巴巴开源库 Fastjson详解
  10. 即时通讯学习笔记004---即时通讯服务器种类认知
  11. iPhone不送充电器?工信部发话了
  12. 树展示 移动端_企业快速搭建移动BI轻应用,怎么少得了这款BI工具
  13. R语言基础期末大作业
  14. 惠普打印机换硒鼓图解_hp硒鼓怎么安装 hp硒鼓安装方法这图文教程】
  15. 软件项目管理 2.2.项目招投标流程
  16. 《凤凰项目 一个IT运维的故事传奇》读后感
  17. LabVIEW编程LabVIEW控制研华PCI 1220运动控制卡例程与相关资料
  18. 徐培成电商项目-徐培成-专题视频课程
  19. 大文件传输的三种方式
  20. cad图纸问号怎么转换文字_CAD中文图纸中文字体变成问号怎么办?不慌这几步教你轻松解决...

热门文章

  1. CentOS_6.5 x86_64 制作U盘启动方法
  2. 2020-08-23 html的标签预加载 + css的新知识 + JS的json的标准格式 + 软技能的能说会道vs安静做事
  3. 网站被攻击怎么处理,快速解决方法
  4. css如何使下划线位置远,使用css调整下划线和文本之间的距离
  5. 谈一谈git revert和revert the revert
  6. Penetration Test 渗透测试
  7. 西电挑战杯文案编写要求
  8. 计算机组成原理——关于原码、补码、移码运算及浮点数运算的总结
  9. UE4蓝图入门知识及UI制作控件事件
  10. 百度团队前端自动化工程工具fis-parser-node-sass插件安装失败的解决方案