ACE Monitor对象.docx
- 文档编号:7743152
- 上传时间:2023-01-26
- 格式:DOCX
- 页数:17
- 大小:134.19KB
ACE Monitor对象.docx
《ACE Monitor对象.docx》由会员分享,可在线阅读,更多相关《ACE Monitor对象.docx(17页珍藏版)》请在冰豆网上搜索。
ACEMonitor对象
ACEMonitor对象:
一个用于并发程序的对象行为模式
(作者:
DouglasC.Schmidt,byhuihoo.orgThzhang译)
目的
Monitor对象模式通过同步方法执行来确保一个对象在一个时刻只有一个方法在被运行。
它也允许一个对象的方法能够协同地安排它们执行的顺序。
别名
线程安全的被动对象
例子
让我们再次考虑在活动对象模式中曾经描述过的通讯网关的设计,参考图一。
网关进程包含多个运行在分离的线程上的供给者(suppliers)处理对象和消费者(consumer)处理对象,这些对象分别的将来自一个或多个远端消息供给者(suppliers)的消息路由给一个或多个远端的消息消费者
(consumer)。
当一个供给者处理对象从一个远端消息供给者接收到消息后,它使用存在消息体中的一个地址域来确定该消息对应的消费者处理对象,这个消费者处理对象最终将消息发送给它的远端消息消费者。
当消息供给者和消息消费者驻留在不同的主机上时,网关使用面向连接的TCP协议来提供可靠的消息发送和端到端的流控。
当发送者发送消息的速度远大于接受者处理消息的速度时,TCP的流控算法将阻塞速度快的发送者。
但是整个网关系统应该是非阻塞的,特别是当等待流控去减少TCP连接的外出流量时。
为了最小化阻塞,每一个消费者处理对象应该包含一个线程安全的消息队列用于缓存从供给处理者线程接受来的新消息。
实现线程安全的消息队列的一种方法是使用活动对象模式,这种模式将调用方法线程和执行方法线程分割开来(译者:
通常情况下方法调用和执行都由调用线程来完成)。
如图二所示,每一个消息队列活动对象包含一个有边界的缓冲和用于维护待处理消息队列的控制线程。
使用活动对象模式实现线程安全的消息队列可以使供给处理对象线程和消费处理对象线程相互分离,于是所有的线程都可以并发运行,当在任意TCP连接上发生流控时,能够被单独阻塞而不影响其他线程的工作。
虽然活动对象模式能够用于实现一个功能化的网关,但是它存在如下的缺陷:
1、性能载荷
活动对象模式提供了一种强有力的并发模型。
它不但能够同步对一个对象上的并发方法请求,而且能够实现完善的调度决策来确定请求的执行顺序。
但是当调度和执行方法请求时,这些特性将引发大量的、重量级的上下文切换、同步、动态内存管理和数据移动带来的负载。
2、编程载荷
活动对象模式要求程序员实现至少6个组件:
代理、方法请求、活动队列、调度器、一个服务和用于每个代理方法的Future。
虽然一些组件比如活动队列和方法请求可以被重用,但是程序员在每次应用该模式的时候可能还是不得不重新实现或客户化这些组件。
一般来说,如果一个应用并不需要活动对象模式中的所有特性,特别是它的"老练的"调度能力,上面提到的性能和编程方面的负载未必会很多。
然而一个开发并发应用的程序员必须保证对一个对象的方法请求应该是被适当的同步或是调度。
语境
存在需要对多线程并发访问对象进行控制的应用。
问题
许多应用都包含能够被多个客户线程并发访问的对象。
因此为了确保并发应用的正确执行,对这些对象的访问经常需要同步或调度。
在解决这个问题之前,下面的三个需求必须被满足:
1、同步的边界应该对应于对象的方法。
面向对象的程序员经常习惯于通过访问对象的接口方法来访问对象,目的是保护对象内部的数据不被任意的改变。
可以直接扩展这个面向对象的编程模式,用于保护对象内部的数据不会被任意的并发改变,这就是通常说的竞争条件。
因此一个对象的接口方法应该被定义成它的同步边界。
2、对象本身,而不是客户,应该实现其自身的方法同步。
并发应用编程将变得更加困难,如果客户端必须清晰的实现获取或释放低层次的同步机制,比如信号量、互斥量以及条件变量。
因此对象本身就有责任确保任何针对它的方法请求的同步应该被透明的进行,而不需要客户清晰的介入。
3、对象能够协同的调度它自身的方法。
如果一个对象的方法在执行过程中必须被阻塞,他们应该自愿的放弃他们的线程控制来保证来自其他客户端线程的方法调用可以访问该对象。
这个特性可以帮助防止死锁,使得影响(或调节)硬件/软件平台上可获得的并发性成为可能。
解决之道
将每一个被客户线程并发访问的对象定义为一个monitor对象。
客户仅仅通过monitor对象的同步方法才能访问monitor对象定义的服务。
为了防止monitor对象的状态陷入竞争条件,在一个时刻只能有一个monitor的同步方法被执行。
每一个monitor对象包含一个monitor锁,被同步方法用于串行访问对象的行为和状态。
此外,同步方法可以根据一个或多个与monitor对象相关的monitorconditions来决定在何种环境下挂起或恢复他们的执行。
结构
在一个monitor对象模式中存在四个参与者:
1、monitor对象。
一个monitor对象向客户暴露一个或多个接口方法。
为了保护monitor对象的内部状态不受任意修改和竞争条件的破坏,所有的客户必须通过这些方法访问monitor对象。
因为monitor本身不包含自己的控制线程,所以每个方法在调用它的客户线程上执行。
2、同步方法。
同步方法实现线程安全的被monitor对象暴露的服务。
为了防止竞争条件,无论是否同时有多个线程并发调用同步方法,还是monitor对象类含有多个同步方法,在一个monitor对象内,在任意时间点只有一个同步方法能够被执行。
3、monitor锁。
每一个monitor对象包含自己的monitor锁。
同步方法使用这个monitor锁来实现每个对象基础上的方法调用串行化。
当方法进入/离开对象时,每个同步方法必须分别的获取/释放monitor锁。
这个协议保证无论什时候一个方法访问或修改对象的状态时都应该先获取monitor锁。
4、monitor条件。
运行在分离线程上的多个同步方法可以经由monitor条件来相互等待和通知以实现协同地调度它们执行的顺序。
同步方法可以使用monitor条件来决定在何种环境下挂起或恢复他们的执行。
动态特征
在monitor对象模式中,在参与者之间将发生如下的协作过程:
1、同步方法的调用和串行化。
当一个客户调用monitor对象的同步方法时,这个方法必须首先获取monitor对象的monitor锁。
只要在monitor对象中有其他同步方法正在被执行,获取monitor锁便不会成功。
在这种情况下,客户线程将被阻塞直到它获取monitor锁,在这个点上同步方法将获取monitor锁,进入临界区,执行方法实现的服务。
一旦同步方法完成执行,monitor锁必须被释放,目的是使其他同步方法可以访问monitor对象。
2、同步方法线程挂起。
如果一个同步方法必须被阻塞或是有其他原因不能立刻进行,它能够在一个monitor条件上等待,这将导致同步方法暂时"离开"monitor对象。
当一个同步方法离开monitor对象,被同步方法获取的monitor锁将自动被释放,客户调用线程将被挂起在monitor条件上。
3、方法条件通知。
一个同步方法能够通知一个monitor条件,目的是为了让一个前期使自己挂起在一个monitor条件上的同步方法线程恢复运行。
此外,一个同步方法能够通知所有的前期使自己挂起在一个monitor条件上的同步方法线程。
4、同步方法线程恢复。
一旦一个早先被挂起在monitor条件上的同步方法线程获取通知,它将继续在最初的等待monitor条件的点上执行。
在被通知线程"重入"monitor对象,恢复执行同步方法之前,monitor锁将自动被获取。
实现
下面的步骤展示了如何实现monitor对象模式。
1、定义monitor对象接口方法。
Monitor对象接口向用户暴露一个方法集合。
典型的接口方法是同步的,在特定的monitor对象中,在任一时刻它们中只有一个能够被执行。
在我们网关的例子中,每一个消费者处理器都包含一个消息队列和TCP连接。
消息队列可以被定义为一个monitor对象,用于缓冲从供给处理器线程接受的消息。
无论什么时候消费者处理器线程遇到发生在连接远端消费者的TCP连接的流控,monitor对象总能够帮助整个网关进程不会被阻塞。
下面的c++程序定义了消息队列monitor对象的接口:
classMessage_Queue
{
public:
enum{
MAX_MESSAGES=/*...*/;
};
//Theconstructordefinesthemaximumnumber
//ofmessagesinthequeue.Thisdetermines
//whenthequeueis'full.'
Message_Queue(size_tmax_messages=MAX_MESSAGES);
//=Messagequeuesynchronizedmethods.
//Puttheatthetailofthequeue.
//Ifthequeueisfull,blockuntilthequeue
//isnotfull.
voidput(constMessage&msg);
//Gettheattheheadofthequeue.
//Ifthequeueisempty,blockuntilthequeue
//isnotempty.
Messageget(void);
//Trueifthequeueisfull,elsefalse.
//Doesnotblock.
boolempty(void)const;
//Trueifthequeueisempty,elsefalse.
//Doesnotblock.
boolfull(void)const;
private:
//...
};
Message_Queuemonitor对象接口暴露了四个同步方法。
Empty和full方法是谓词,客户可以用来区分三个不同的状态:
空、满、既不空也不满。
Put和get方法分别用于完成从消息队列中插入和提取消息,如果消息队列为满或空将发生阻塞。
2、定义接口方法的对象实现方法。
一个monitor对象经常包含实现方法用于简化其接口方法。
这个分离的概念有助于实现monitor对象的功能和同步及调度逻辑之间的解耦合,同时也避免了对象内部的死锁和不必要的加锁载荷。
下面的惯例是基于线程安全的接口的习惯用法,可以用于构建接口方法和实现方法相分离的概念。
a)接口方法仅仅是获取/释放monitor锁,等待/通知特定的monitor条件,将monitor对象的功能转交给实现方法来完成。
b)当实现方法被接口方法调用时,仅仅实现其特定的功能,不再进行获取/释放monitor锁,等待/通知特定的monitor条件。
此外,为了避免对象内部的死锁和不必要的加锁载荷,实现方法不应该调用任何monitor对象接口中定义的同步方法。
在我们网关的例子中,Message_Queue类定义了四个实现方法:
put_i、get_i、empty_i和full_i,分别对应于相应的接口方法。
这些函数的签名如下:
classMessage_Queue
{
public:
//...Seeabove....
private:
//=Privatehelpermethods(non-synchronized
//anddonotblock).
//Puttheatthetailofthequeue.
voidput_i(constMessage&msg);
//Gettheattheheadofthequeue.
Messageget_i(void);
//Trueifthequeueisfull,elsefalse.
//Assumeslocksareheld.
boolempty_i(void)const;
//Trueifthequeueisempty,elsefalse.
//Assumeslocksareheld.
boolfull_i(void)const;
//...
典型的这些方法既不是同步的,也不会被阻塞,刚好符合上面提到的线程安全接口的习惯用法。
3、定义方法的对象内部状态。
一个monitor对象包含定义在其内部状态的数据成员。
此外,一个monitor对象包含一个用于串行化它的同步方法执行的monitor锁,一个或多个用于调度同步方法执行的monitor条件。
对于每一个类型的状况都有一个单独的monitor条件,在这里同步方法必须使它们被挂起或恢复其他被同步方法挂起的线程。
一个monitor锁可以用mutex来实现。
当获得锁的线程正在执行临界区中的代码时,mutex用来使其他试图进入临界区的线程等待。
Monitor条件可以用条件变量来实现。
与mutex不同的是,条件变量被线程用来使自己被阻塞直到一个涉及共享数据的任意复杂条件表达式达到某个特殊的状态。
一个条件变量经常和mutex结合起来被使用,客户线程在进行表达式评估之前必须先获取mutex。
如果条件表达式返回flase,客户线程将自动在条件变量上把自己挂起,同时释放mutex,从而使其他线程可以获取mutex来修改共享数据。
当相应的线程更改了共享数据后,它可以通知条件变量,条件变量将自动的恢复早先在其上面挂起的线程并再次获取mutex。
获取mutex的恢复线程再次评估条件表达式,如果共享数据达到希望的状态,线程将继续执行下去。
否则,线程将重新使自己挂起在条件变量上等待它再次被恢复。
这个过程将一直重复到条件表达式返回true.
一般情况下,条件变量比mutex更适合用于处理包含复杂表达式的状态或是调度行为。
举例来说,条件变量可以被用来实现线程安全的消息队列。
在这个用例中,一对条件变量可以协作的用来分别阻塞供给者线程,当消息队列满的时候和阻塞消费者线程,当消息队列空的时候。
在我们网关的例子中,Message_Queue类定义了它的内部状态,如下所示:
classMessage_Queue
{
//...Seeabove....
private:
//InternalQueuerepresentation....
//Currentnumberofsinthequeue.
size_tmessage_count_;
//Themaximumnumbersthatcanbe
//inaqueuebeforeit'sconsidered'full.'
size_tmax_messages_;
//=Mechanismsrequiredtoimplementthe
//monitorobject'ssynchronizationpolicies.
//Mutexthatprotectthequeue'sinternalstate
//fromraceconditionsduringconcurrentaccess.
mutableThread_Mutexmonitor_lock_;
//Conditionvariableusedtomakesynchronized
//methodthreadswaituntilthequeueisnolongerempty.
Thread_Conditionnot_empty_;
//Conditionvariableusedtomakesynchronized
//methodthreadswaituntilthequeueisnolongerfull.
Thread_Conditionnot_full_;
}
一个Message_Queuemonitor对象定义了三种类型的状态:
a)队列表现数据成员。
这些数据成员定义了内部消息队列的表现形式。
这个表现使用循环数组或是链表来存储队列中的内容,同时还包含用于确定队列是否空、满、非空非满状态的记帐信息。
内部消息队列的表现仅仅通过get_i、put_i、full_i和empty_i四个实现方法来访问和操纵。
b)monitor锁数据成员。
Monitor锁被Message_Queue的同步方法用于串行化对monitor对象的访问。
Monitor锁使用在包装门面模式(wrapperfa?
adepattern)中定义的ThreadMutex类来实现。
这个类提供了与平台无关的API接口。
c)monitor条件数据成员。
当Message_Queue在空和满的边界条件转化时,Monitor条件被put和get同步方法分别用于使自己挂起和恢复。
这些monitor条件使用如下定义的ThreadCondition包装门面类来实现:
classThread_Condition
{
public:
//Initializetheconditionvariableand
//associateitwiththe.
Thread_Condition(constThread_Mutex&m)
//Implicitlydestroytheconditionvariable.
Thread_Condition(void);
//Waitforthetobe,
//notifiedoruntilhaselapsed.
//If==0waitindefinitely.
intwait(Time_Value*timeout=0)const;
//Notifyonethreadwaitingonthe.
intnotify(void)const;
//Notify*all*threadswaitingonthe.
intnotify_all(void)const;
private:
#ifdefined(_POSIX_PTHREAD_SEMANTICS)
pthread_cond_tcond_;
#else
//Conditionvariableemulations.
#endif/*_POSIX_PTHREAD_SEMANTICS*/
//Referencetomutexlock.
constThread_Mutex&mutex_;
};
构造函数初始化条件变量,并使它与通过参数传递的Thread_Mutex相互关联。
析构函数销毁条件变量,释放任何由构造函数分配的资源。
需要注意的是mutex_本身不被Thread_Condition所拥有,所以在析构函数中不要销毁mutex_对象。
当被客户线程调用的时候,wait方法将自动释放mutex_对象,并使线程挂起一直到超过等待其他线程通知Thread_Condition对象的时间。
Notify方法恢复一个等待在Thread_Condition对象上的线程,notifyall同时所有并发等待在Thread_Condition对象上的线程。
在wait方法或者因为条件变量被通知,或者因为超时而返回客户线程之前,mutex_对象将被再次获取。
4、实现所有的monitor对象的方法和数据成员。
最后这一步包括实现所有monitor对象的方法和上面定义的内部状态。
a)初始化数据成员。
这个子步骤实现对象指定数据成员的初始化,包括monitor锁和所有的monitor条件。
例如:
Message_Queue的构造函数创建一个空的消息队列,初始化monitor条件not_empty_和not_full_
Message_Queue:
:
Message_Queue(size_tmax_messages)
:
not_full_(monitor_lock_),
not_empty_(monitor_lock_),
max_messages_(max_messages),
message_count_(0)
{
//...
}
在这个例子中,展示了monitor条件如何共享同一个monitor锁。
当多个线程同时通过put和get方法访问消息队列时,这个设计保证了Message_Queue对象的状态,如message_count,能够被串行化访问用于防止竞争条件的发生。
b)使用线程安全接口模式。
在这个子步骤中,根据线程安全接口模式,接口方法和实现方法被分别的实现。
例如,下面的Message_Queue方法用于检测队列是否是空,也就是队列中没有任何消息,或是满,也就是队列中包含了超过maxmessages数量的消息。
我们先看看接口方法的实现:
bool
Message_Queue:
:
empty(void)const
{
Guardguard(monitor_lock_);
returnempty_i();
}
bool
Message_Queue:
:
full(void)const
{
Guardguard(monitor_lock_);
returnfull_i();
}
这些方法展示了上面提到的线程安全接口模式的一个简单的例子。
它们使用范围锁习惯用法来获取/释放monitor锁,然后立刻调用相应的实现方法。
正像下面所要展示的,这些实现方法都是在假定monitor锁已经被获取前提下,进行简单的队列边界条件检测。
bool
Message_Queue:
:
empty_i(void)const
{
returnmessage_count_<=0;
}
bool
Message_Queue:
:
full_i(void)const
{
returnmessage_count_>max_messages_;
}
put方法在消息队列的尾部添加一个新的消息。
它是一个同步方法,下面展示了更加老练的使用线程安全接口模式的方法实现。
Message_Queue:
:
put(constMessage&msg)
{
//UsetheScopedLockingidiomto
//acquire/releasetheupon
//entry/exittothesynchronizedmethod.
Guardguard(monitor_lock_);
//Waitwhilethequeueisfull.
while(full_i()){
//Releaseandsuspendour
//threadwaitingforspacetobecomeavailable
//inthequeue.Theis
//reacquiredautomaticallywhenreturns.
not_full_.wait();
}
//Enqueuetheatthetailof
//thequeueandupdat
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- ACE Monitor对象 Monitor 对象
![提示](https://static.bdocx.com/images/bang_tan.gif)