主动对象用于并发编程的对象行为模式.docx
- 文档编号:26702031
- 上传时间:2023-06-21
- 格式:DOCX
- 页数:41
- 大小:214.16KB
主动对象用于并发编程的对象行为模式.docx
《主动对象用于并发编程的对象行为模式.docx》由会员分享,可在线阅读,更多相关《主动对象用于并发编程的对象行为模式.docx(41页珍藏版)》请在冰豆网上搜索。
主动对象用于并发编程的对象行为模式
第6章主动对象(ActiveObject):
用于并发编程的对象行为模式
R.GregLavenderDouglasC.Schmidt
摘要
本论文描述主动对象(ActiveObject)模式。
该模式使方法执行与方法调用去耦合,以简化对驻留在它自己的线程控制中的对象的同步访问。
主动对象模式允许一或多个交错访问数据的独立执行的线程被建模为单个对象。
这一并发模式能良好地适用于广泛的生产者/消费者和读者/作者应用类。
该模式通常用于需要多线程服务器的分布式系统中。
此外,客户应用,比如窗口系统和网络浏览器,采用主动对象来简化并发和异步的网络操作。
6.1意图
主动对象设计模式使方法执行与方法调用去耦合,以增强并发、并简化对驻留在它自己的线程控制中的对象的同步访问。
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,例如,通过允许应用并行地处理多个客户请求。
并发对象驻留在它们自己的线程控制中,而不是使用单线程被动对象棗这些对象在调用其方法的客户的线程控制中执行它们的方法。
但是,如果对象并发执行,且这些对象被多个客户线程共享,我们必须同步对它们的方法和数据的访问。
在存在这样的问题时,会产生三种压力:
1.对对象方法的并发调用不应阻塞整个进程,以免降低其他方法的QoS:
例如,如果在我们的网关例子中,一个外出的到消费者的TCP连接因为流控制而阻塞,网关进程仍应该能在等待流控制缓解的同时,排队新的消息。
同样地,如果其他外出的TCP连接没有被流控制,它们应该能独立于任何阻塞连接发送消息给它们的消费者。
2.对共享对象的同步访问应该很简单:
如果开发者必须显式地使用低级同步机制,比如像获取和释放互斥锁(mutex),常常难于对网关这样的应用进行编程。
一般而言,当对象被多个客户线程访问时,需进行同步约束的方法应该被透明地序列化。
3.应用应设计为能透明地利用硬件/软件平台上可用的并行机制:
在我们的网关例子中,发往不同消费者的消息应该被网关并行地在不同的TCP连接上发送。
但是,如果整个网关被编写为仅在单个线程控制中运行,性能瓶颈就不可能通过在多处理器上运行网关而被透明地克服。
6.6解决方案
对于每个需要并发执行的对象,使对对象方法的请求与方法执行去耦合。
这样的去耦合被设计用于使客户线程看起来像是调用一个平常的方法。
该方法被自动转换为方法请求对象,并传递给另一个线程控制,在其中它又被转换回方法,并在对象实现上被执行。
主动对象由以下组件组成:
代理(Proxy)[5,2]表示对象的接口,仆人(Servant)提供对象的实现。
代理和仆人运行在分离的线程中,以使方法调用和方法执行能并发运行:
代理在客户线程中运行,而仆人在不同的线程中运行。
在运行时,代理将客户的方法调用(MethodInvocation)转换为方法请求(MethodRequest),并由调度者(Scheduler)将其存储在启用队列(ActivationQueue)中。
调度者持续地运行在与仆人相同的线程中,当启用队列中的方法请求变得可运行时,就将它们出队,并分派给实现主动对象的仆人。
客户可通过代理返回的“期货”(future)获取方法执行的结果。
6.7结构
主动对象模式的结构在下面的Booch类图中演示:
在主动对象模式中有六个关键的参与者:
代理(Proxy)
∙代理提供一个接口,允许客户使用标准的强类型程序语言特性,而不是在线程间传递松散类型的消息,来调用主动对象的可公共访问的方法。
当客户调用代理定义的方法时,就会在调度者的启用队列上触发方法请求对象的构造和排队;所有这些都发生在客户的线程控制中。
方法请求(MethodRequest)
∙方法请求用于将代理上的特定方法调用的上下文信息,比如方法参数和代码,从代理传递给运行在分离线程中的调度者。
抽象方法请求类为执行主动对象方法定义接口。
该接口还包含守卫(guard)方法,可用于确定何时方法请求的同步约束已被满足。
对于代理提供的每个主动对象方法(它们在其仆人中需要同步的访问),抽象方法请求类被子类化,以创建具体的方法请求类。
这些类的实例在其方法被调用时由代理创建,并包含了执行这些方法调用和返回任何结果给客户所需的特定的上下文信息。
启用队列(ActivationQueue)
∙启用队列维护一个有界缓冲区,内有代理创建的待处理的方法请求。
该队列跟踪哪些方法请求将要执行。
它还使客户线程与仆人线程去耦合,以使两个线程能并发运行。
调度者(Scheduler)
∙调度者运行在与其客户不同的线程中,它管理待处理的方法请求的启用队列。
调度者决定下一个出队的方法请求,并在实现该方法的仆人上执行。
这样的调度决策基于各种标准,比如像方法被插入到启用队列中的顺序;以及同步约束,例如特定属性的满足或特定事件的发生,比如在有界数据结构中有新的条目空间变得可用。
调度者通常使用方法请求守卫来对同步约束进行求值。
仆人(Servant)
∙仆人定义被建模为主动对象的行为和状态。
它实现在代理中定义的方法及相应的方法请求。
仆人方法在调度者执行其相应的方法请求时被调用;因而,仆人在调度者的线程控制中执行。
仆人还可提供其他方法,由方法请求用于实现它们的守卫。
期货(Future)
∙期货[7,8]允许客户在仆人结束方法的执行后获取方法调用的结果。
当客户通过代理调用方法时,期货被立即返回给客户。
期货为被调用的方法保留空间,以存储它的结果。
当客户想要获取这些结果时,它可以阻塞或者轮询,直到结果被求值和存储到期货中,然后与期货“会合”。
6.8动力特性
下图演示主动对象模式中的协作的三个阶段:
1.方法请求构造和调度:
在此阶段,客户调用代理上的方法,从而触发方法请求的创建;方法请求维护方法的参数绑定,以及其他任何执行方法和返回结果所需的绑定。
代理随后将方法请求传递给调度者,后者将其放入启用队列中。
如果方法被定义为“两路”(twoway)[6]的,一个期货的绑定被返回给调用该方法的客户。
如果方法被定义为“单路”(oneway)的,就没有期货被返回,也就是,它没有返回值。
2.方法执行:
在此阶段,调度者在与其客户不同的线程中持续运行。
在此线程中,调度者监控启用队列,并确定哪些方法请求已成为可运行的,例如,当它们的同步约束已被满足时。
当方法请求成为可运行的,调度者就使其出队,绑定到仆人,并分派仆人上的适当方法。
当此方法被调用时,它可以访问/更新它的仆人的状态并创建它的结果。
3.完成:
在最后的阶段中,结果(如果有的话)被存储在期货中,而调度者持续地监控启用队列中,看是否有可运行的方法请求。
在一个两路方法完成后,客户可以通过与期货会合来获取它的结果。
一般而言,任何与期货会合的客户都可以获取它的结果。
当方法请求和期货不再被引用时,它们就被删除或被垃圾回收。
6.9实现
这一部分解释使用主动对象模式构建并发应用所涉及的步骤。
使用主动对象模式实现的应用是6.3网关的一部分。
图6-2演示该例子的结构和参与者。
这一部分中的例子使用ACE构架[9]的可复用组件。
ACE提供了一组丰富的可复用C++包装和构架组件,可跨越广泛的OS平台执行常见的通信软件任务。
1.实现仆人:
仆人定义被建模为主动对象的行为和状态。
客户可通过代理来访问仆人所实现的方法。
此外,仆人还可包含其他方法,方法请求可以用这些方法来实现守卫,以允许调度者对运行时同步约束进行求值。
这些约束决定调度者分派方法请求的顺序。
在我们的网关例子中,仆人是一个消息队列,缓冲待处理的递送给消费者的消息。
对于每一个远地消费者,都有一个ConsumerHandler(消费者处理器),其中含有一个到消费者进程的TCP连接。
此外,ConsumerHandler含有一个被建模为主动对象的消息队列,并通过MQ_Servant来实现。
当从供应者传递到网关的消息在等待被发送到它们的远地消费者时,每个ConsumerHandler的主动对象消息队列就存储这些消息。
下面的类提供了这个仆人的接口:
classMQ_Servant
{
public:
MQ_Servant(size_tmq_size);
//Messagequeueimplementationoperations.
voidput_i(constMessage&msg);
Messageget_i(void);
//Predicates.
boolempty_i(void)const;
boolfull_i(void)const;
private:
//InternalQueuerepresentation,e.g.,a
//circulararrayoralinkedlist,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]问题;如果子类需要不同的同步策略,该问题将会妨碍仆人实现的复用。
因而,对主动对象的同步约束的改变不需要影响它的仆人实现。
2.实现代理和方法请求:
代理为客户提供仆人方法的接口。
对于客户的每一次方法调用,代理都会创建一个方法请求。
方法请求是方法上下文的抽象。
该上下文通常包括方法参数、到将要应用此方法的仆人的绑定、结果期货,以及方法请求的call方法的代码。
在我们的网关例子中,MQ_Proxy提供步骤1中定义的MQ_Servant的抽象接口。
该消息队列被ConsumerHandler用于排队递送给消费者的消息,如图6-2所示。
此外,MQ_Proxy还是一个工厂,它构造方法请求的实例,并将它们传递给调度者,后者将它们排队,用于后面在分离的线程中的执行。
MQ_Proxy的C++实现如下所示:
classMQ_Proxy
{
public:
//Boundthemessagequeuesize.
enum{MAX_SIZE=100};
MQ_Proxy(size_tsize=MAX_SIZE)
:
scheduler_(newMQ_Scheduler(size)),
servant_(newMQ_Servant(size)){}
//Schedule
voidput(constMessage&m)
{
Method_Request*method_request=newPut(servant_,m);
scheduler_->enqueue(method_request);
}
//ReturnaMessage_Futureasthe‘‘future’’
//resultofanasynchronous
//methodontheactiveobject.
Message_Futureget(void)
{
Message_Futureresult;
Method_Request*method_request=newGet(servant_,result);
scheduler_->enqueue(method_request);
returnresult;
}
//...empty()andfull()predicateimplementations...
protected:
//TheServantthatimplementsthe
//ActiveObjectmethods.
MQ_Servant*servant_;
//AschedulerfortheMessageQueue.
MQ_Scheduler*scheduler_;
};
MQ_Proxy的每个方法都将它的调用转换为方法请求,并将其传递给它的MQ_Scheduler,后者将请求入队,用于后续的启用。
Method_Request基类定义虚guard和call方法,分别被它的调度者用于决定方法请求是否可被执行和在它的仆人上执行方法请求。
如下所示:
classMethod_Request
{
public:
//Evaluatethesynchronizationconstraint.
virtualboolguard(void)const=0;
//Implementthemethod.
virtualvoidcall(void)=0;
};
该类中的方法必须被子类定义,代理中定义的每个方法都有一个相应的Method_Request子类。
定义这两个方法的原因是为调度者提供一个统一接口来计算和执行具体Method_Request。
因而,调度者就得以与怎样计算同步约束、或是触发具体Method_Request执行的特定知识去耦合。
例如,在我们的网关例子中,当客户调用代理上的put方法时,该方法被转换为Put子类的实例;该子类继承自Method_Request,并含有指向MQ_Servant的指针。
如下所示:
classPut:
publicMethod_Request
{
public:
Put(MQ_Servant*rep,Messagearg)
:
servant_(rep),arg_(arg){}
virtualboolguard(void)const
{
//Synchronizationconstraint:
onlyallow
//
return!
servant_->full_i();
}
virtualvoidcall(void)
{
//Insertmessageintotheservant.
servant_->put_i(arg_);
}
private:
MQ_Servant*servant_;
Messagearg_;
};
注意guard方法怎样使用MQ_Servant的full_I断言来实现同步约束,以允许调度者确定Put方法请求何时可以执行。
当Put方法请求可被执行时,调度者调用它的call挂钩方法。
该方法使用它的MQ_Servant运行时绑定来调用仆人的put_i方法。
put_i方法在仆人的上下文中执行,并且不需要任何显式的序列化机制,因为调度者通过方法请求guard来强制实施所有必要的同步约束。
代理还将get方法转换为Get类的实例;Get类定义如下:
classGet:
publicMethod_Request
{
public:
Get(MQ_Servant*rep,constMessage_Future&f)
:
servant_(rep),result_(f){}
boolguard(void)const
{
//Synchronizationconstraint:
//cannotcalla
//thequeueisnotempty.
return!
servant_->empty_i();
}
virtualvoidcall(void)
{
//Bindthedequeuedmessagetothe
//futureresultobject.
result_=servant_->get_i();
}
private:
MQ_Servant*servant_;
//Message_Futureresultvalue.
Message_Futureresult_;
};
对于代理中所有返回值的两路方法,比如在我们的网关例子中的get_i方法,Message_Future被返回给调用该方法的客户线程,如下面的实现步骤4所示。
客户可以选择立即对Message_Future的值进行求值,在这样的情况下,客户会阻塞、直到方法请求被调度者执行为止。
相反,对主动对象方法调用的返回结果的求值也可被延期,在这样的情况下,客户线程和执行该方法的线程可以异步地执行。
3.实现启用队列:
每个方法请求都被放入启用队列中。
启用队列通常实现为线程安全的有界缓冲区,由客户线程与调度者及仆人的线程共享。
启用队列还提供一个迭代器,允许调度者能依照迭代器模式[5]遍历它的元素。
下面的C++代码演示Activation_Queue是怎样被用于网关中的:
classActivation_Queue
{
public:
//Blockforan"infinite"amountoftime
//waitingfor
//tocomplete.
constintINFINITE=-1;
//Definea"trait".
typedefActivation_Queue_Iteratoriterator;
//Constructorcreatesthequeuewiththe
//specifiedhighwatermarkthatdetermines
//itscapacity.
Activation_Queue(size_thigh_water_mark);
//Insert
//upto
//tobecomeavailableinthequeue.
voidenqueue(Method_Request*method_request,
longmsec_timeout=INFINITE);
//Remove
//upto
//
voiddequeue(Method_Request*method_request,
longmsec_timeout=INFINITE);
private:
//Synchronizationmechanisms,e.g.,condition
//variablesandmutexes,andthequeue
//implementation,e.g.,anarrayoralinked
//list,gohere.
//...
};
enqueue和dequeue方法提供一种“有界缓冲区生产者/消费者”并发模式,允许多个线程同时插入和移除Method_Request,而不会破坏Activation_Queue的内部状态。
一或多个客户线程扮演生产者角色,通过代理将Method_Request入队。
调度者线程扮演消费者角色,当Method_Request的guard方法求值为“真”时,将它们出队,并调用它们的call挂钩来执行仆人方法。
Activation_Queue被设计为使用条件变量和互斥体[14]的有界缓冲区。
因此,当试图从空的Activation_Queue中移除Method_Request时,调度者将会阻塞msec_timeout长度的时间。
同样地,当试图在满的Activation_Queue中(也就是,当前Method_Request数目等于其高水位标的队列)插入时,客户线程将会阻塞最多msec_timeout长度的时间。
如果enqueue方法超时了,控制会返回给客户线程,而方法没有被执行。
4.实现调度者:
调度者维护启用队列,并执行同步约束已满足的待处理方法请求。
调度者的公共接口通常为代理提供一个方法,用于将方法请求放入启用队列中;还有另一个方法,用于在仆人上分派方法请求。
这些方法运行在分离的线程中,也就是,代理运行在与调度者和仆人不同的线程中,而后两者运行在同一线程中。
在我们的网关例子中,我们定义MQ_Scheduler类如下:
classMQ_Scheduler
{
public:
//InitializetheActivation_Queuetohavethe
//specifiedcapacityandmaketheScheduler
//runinitsownthreadofcontrol.
MQ_Scheduler(size_thigh_water_mark);
//...Otherconstructors/des
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 主动 对象 用于 并发 编程 行为 模式