摘 要
本论文描述主动对象(Active Object)模式。该模式使方法执行与方法调用去耦合,以简化对驻留在它自己的线程控制中的对象的同步访问。主动对象模式允许一或多个交错访问数据的独立执行的线程被建模为单个对象。这一并发模式能良好地适用于广泛的生产者/消费者和读者/作者应用类。该模式通常用于需要多线程服务器的分布式系统中。此外,客户应用,比如窗口系统和网络浏览器,采用主动对象来简化并发和异步的网络操作。
6.1 意图
主动对象设计模式使方法执行与方法调用去耦合,以增强并发、并简化对驻留在它自己的线程控制中的对象的同步访问。
6.2 别名
6.2 别名
并发对象和Actor。
6.3 例子
为演示主动对象模式,考虑一个通信网关[1]的设计。网关使协作的组件去耦合,并允许它们进行交互,而无需彼此直接依赖[2]。图6-1中所示的网关在分布式系统中将来自一或多个供应者进程的消息路由到一或多个消费者进程[3]。
在我们的例子中,网关、供应者和消费者在面向连接的协议TCP[4]之上进行通信。因此,当网关软件尝试向远地消费者发送数据时,可能会遇到来自TCP传输层的流控制。TCP使用流控制来确保快速的生产者或网关不会过快地生产出数据,以致慢速消费者或拥挤的网络不能缓冲和处理这些数据。
为了改善所有供应者和消费者的端到端服务质量(QoS),整个网关进程不能在任何到消费者的连接上阻塞以等待流控制缓解。此外,当供应者和消费者的数目增加时,网关还必须能高效地扩展,
防止阻塞并提高性能的一种有效的方法是在网关设计中引入并发。并发应用允许执行对象O的方法的线程控制与调用O的方法的线程控制去耦合。而且,在网关中使用并发还使TCP连接被流控制的线程的阻塞不会阻碍TCP连接未被流控制的线程的执行。
图6-1 通信网关
6.4 上下文
对运行在相互分离的线程控制中的对象进行访问的客户。
6.5 问题
许多应用受益于使用并发对象来改善它们的QoS,例如,通过允许应用并行地处理多个客户请求。并发对象驻留在它们自己的线程控制中,而不是使用单线程 被动对象棗这些对象在调用其方法的客户的线程控制中执行它们的方法。但是,如果对象并发执行,且这些对象被多个客户线程共享,我们必须同步对它们的方法和数据的访问。在存在这样的问题时,会产生三种压力:
- 对对象方法的并发调用不应阻塞整个进程,以免降低其他方法的QoS:例如,如果在我们的网关例子中,一个外出的到消费者的TCP连接因为流控制而阻塞,网关进程仍应该能在等待流控制缓解的同时,排队新的消息。同样地,如果其他外出的TCP连接没有被流控制,它们应该能独立于任何阻塞连接发送消息给它们的消费者。
- 对共享对象的同步访问应该很简单:如果开发者必须显式地使用低级同步机制,比如像获取和释放互斥锁(mutex),常常难于对网关这样的应用进行编程。一般而言,当对象被多个客户线程访问时,需进行同步约束的方法应该被透明地序列化。
- 应用应设计为能透明地利用硬件/软件平台上可用的并行机制:在我们的网关例子中,发往不同消费者的消息应该被网关并行地在不同的TCP连接上发送。但是,如果整个网关被编写为仅在单个线程控制中运行,性能瓶颈就不可能通过在多处理器上运行网关而被透明地克服。
6.6 解决方案
对于每个需要并发执行的对象,使对对象方法的请求与方法执行去耦合。这样的去耦合被设计用于使客户线程看起来像是调用一个平常的方法。该方法被自动转换为方法请求对象,并传递给另一个线程控制,在其中它又被转换回方法,并在对象实现上被执行。
主动对象由以下组件组成: 代理(Proxy)[5, 2]表示对象的接口, 仆人(Servant)提供对象的实现。代理和仆人运行在分离的线程中,以使方法调用和方法执行能并发运行:代理在客户线程中运行,而仆人在不同的线程中运行。在运行时,代理将客户的方法调用(Method Invocation)转换为 方法请求(Method Request),并由 调度者(Scheduler)将其存储在 启用队列(Activation Queue)中。调度者持续地运行在与仆人相同的线程中,当启用队列中的方法请求变得可运行时,就将它们出队,并分派给实现主动对象的仆人。客户可通过代理返回的“ 期货”(future)获取方法执行的结果。
6.7 结构
主动对象模式的结构在下面的Booch类图中演示:
在主动对象模式中有六个关键的参与者:
代理(Proxy )
- 代理提供一个接口,允许客户使用标准的强类型程序语言特性,而不是在线程间传递松散类型的消息,来调用主动对象的可公共访问的方法。当客户调用代理定义的方法时,就会在调度者的启用队列上触发方法请求对象的构造和排队;所有这些都发生在客户的线程控制中。
方法请求(Method Request )
- 方法请求用于将代理上的特定方法调用的上下文信息,比如方法参数和代码,从代理传递给运行在分离线程中的调度者。抽象方法请求类为执行主动对象方法定义接口。该接口还包含守卫(guard)方法,可用于确定何时方法请求的同步约束已被满足。对于代理提供的每个主动对象方法(它们在其仆人中需要同步的访问),抽象方法请求类被子类化,以创建具体的方法请求类。这些类的实例在其方法被调用时由代理创建,并包含了执行这些方法调用和返回任何结果给客户所需的特定的上下文信息。
启用队列(Activation Queue )
- 启用队列维护一个有界缓冲区,内有代理创建的待处理的方法请求。该队列跟踪哪些方法请求将要执行。它还使客户线程与仆人线程去耦合,以使两个线程能并发运行。
调度者(Scheduler )
- 调度者运行在与其客户不同的线程中,它管理待处理的方法请求的启用队列。调度者决定下一个出队的方法请求,并在实现该方法的仆人上执行。这样的调度决策基于各种标准,比如像方法被插入到启用队列中的顺序;以及同步约束,例如特定属性的满足或特定事件的发生,比如在有界数据结构中有新的条目空间变得可用。调度者通常使用方法请求守卫来对同步约束进行求值。
仆人(Servant )
- 仆人定义被建模为主动对象的行为和状态。它实现在代理中定义的方法及相应的方法请求。仆人方法在调度者执行其相应的方法请求时被调用;因而,仆人在调度者的线程控制中执行。仆人还可提供其他方法,由方法请求用于实现它们的守卫。
期货(Future )
- 期货[7, 8]允许客户在仆人结束方法的执行后获取方法调用的结果。当客户通过代理调用方法时,期货被立即返回给客户。期货为被调用的方法保留空间,以存储它的结果。当客户想要获取这些结果时,它可以阻塞或者轮询,直到结果被求值和存储到期货中,然后与期货“会合”。
6.8 动力 特性
下图演示主动对象模式中的协作的三个阶段:
- 方法请求构造和调度:在此阶段,客户调用代理上的方法,从而触发方法请求的创建;方法请求维护方法的参数绑定,以及其他任何执行方法和返回结果所需的绑定。代理随后将方法请求传递给调度者,后者将其放入启用队列中。如果方法被定义为“两路”(two way)[6]的,一个期货的绑定被返回给调用该方法的客户。如果方法被定义为“单路”(oneway)的,就没有期货被返回,也就是,它没有返回值。
- 方法执行:在此阶段,调度者在与其客户不同的线程中持续运行。在此线程中,调度者监控启用队列,并确定哪些方法请求已成为可运行的,例如,当它们的同步约束已被满足时。当方法请求成为可运行的,调度者就使其出队,绑定到仆人,并分派仆人上的适当方法。当此方法被调用时,它可以访问/更新它的仆人的状态并创建它的结果。
- 完成:在最后的阶段中,结果(如果有的话)被存储在期货中,而调度者持续地监控启用队列中,看是否有可运行的方法请求。在一个两路方法完成后,客户可以通过与期货会合来获取它的结果。一般而言,任何与期货会合的客户都可以获取它的结果。当方法请求和期货不再被引用时,它们就被删除或被垃圾回收。
6.9 实现
这一部分解释使用主动对象模式构建并发应用所涉及的步骤。使用主动对象模式实现的应用是6.3网关的一部分。图6-2演示该例子的结构和参与者。这一部分中的例子使用ACE构架[9]的可复用组件。ACE提供了一组丰富的可复用C++包装和构架组件,可跨越广泛的OS平台执行常见的通信软件任务。
- 实现仆人:仆人定义被建模为主动对象的行为和状态。客户可通过代理来访问仆人所实现的方法。此外,仆人还可包含其他方法,方法请求可以用这些方法来实现守卫,以允许调度者对运行时同步约束进行求值。这些约束决定调度者分派方法请求的顺序。
在我们的网关例子中,仆人是一个消息队列,缓冲待处理的递送给消费者的消息。对于每一个远地消费者,都有一个Consumer Handler(消费者处理器),其中含有一个到消费者进程的TCP连接。此外,Consumer Handler含有一个被建模为主动对象的消息队列,并通过MQ_Servant来实现。当从供应者传递到网关的消息在等待被发送到它们的远地消费者时,每个Consumer Handler的主动对象消息队列就存储这些消息。下面的类提供了这个仆人的接口:
class MQ_Servant
{
public:
MQ_Servant (size_t mq_size);
// Message queue implementation operations.
void put_i (const Message &msg);
Message get_i (void);
// Predicates.
bool empty_i (void) const;
bool full_i (void) const;
private:
// Internal Queue representation, e.g., a
// circular array or a linked list, etc.
};
图6-2 将消费者处理器的消息队列实现为主动对象
put_i和get_i方法分别实现队列的插入和移除操作。此外,仆人还定义了两个断言(Predicate):empty_i和full_i,可区分三种内部状态(1)空,(2)满,以及(3)既不为空也不为满。这些断言用于方法请求的看守方法的实现,后者允许调度者强制实施运行时同步约束;这些同步约束规定仆人的put_i和get_i的调用顺序。
注意MQ_Servant类是怎样设计,以使同步机制始终外在于仆人。例如,在我们的网关例子中,MQ_Servant类中的方法并不包括任何实现同步的代码。该类仅仅提供实现仆人功能和检查它的内部状态的方法。这样的设计避免了“ 继承异常”[10, 11, 12, 13]问题;如果子类需要不同的同步策略,该问题将会妨碍仆人实现的复用。因而,对主动对象的同步约束的改变不需要影响它的仆人实现。
毛毛的小窝
毛毛的小窝