任務和主動對象(Active Object):併發編程模式

這一章介紹前面提到過的ACE_Task類,另外還介紹了主動對象模式。基本上這一章將涵蓋兩個主題。首先,它將講述怎樣將ACE_Task構造作爲高級面向對象機制使用,用以編寫多線程程序。其次,它將討論怎樣在主動對象模式[II]中使用ACE_Task

 

 

  那麼到底什麼是主動對象呢?傳統上,所有的對象都是被動的代碼段,對象中的代碼是在對它發出方法調用的線程中執行的。也就是,調用線程(

 

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

可用作:

  • 更高級的線程(我們稱之爲任務)。
  • 主動對象模式中的主動對象。

5.2.1 任務的結構

  ACE_Task

 

的結構在本質上與基於Actor的系統[III]中的“Actor

”的結構相類似。該結構如下所示:

 

 

 

5-1

任務結構示意圖

 

 

5-1說明每個任務都含有一或多個線程,以及一個底層消息隊列。各個任務通過這些消息隊列進行通信。但是,消息隊列並非是程序員需要關注的對象。發送任務可以使用putq()調用來將消息插入到另一任務的消息隊列中。隨後接收任務就可以通過使用getq()

調用來從它自己的消息隊列裏將消息提取出來。

  因而,你可以設想一個系統,由多個自治的任務(或主動對象)構成,這些任務通過它們的消息隊列相互通信。這樣的體系結構有助於大大簡化多線程程序的編程模型。

 

5.2.2 創建和使用任務

  如上面所提到的,要創建任務或主動對象,你必須從

 

ACE_Task

類派生子類。在子類派生之後,必須採取以下步驟:

 

實現服務初始化和終止方法: open()方法應該包含所有專屬於任務的初始化代碼。其中可能包括諸如連接控制塊、鎖和內存這樣的資源。close() 方法是相應的終止方法。 Activation)方法:在主動對象實例化後,你必須通過調用activate()啓用它。要在主動對象中創建的線程的數目,以及其他一些參數,被傳遞給activate()方法。activate()方法會使svc() 方法成爲所有它生成的線程的啓動點。 svc() 方法中啓動。應用開發者必須在子類中定義此方法。

 

  下面的例子演示怎樣去創建任務:

 

 

5-1

#include "ace/OS.h"

#include "ace/Task.h"

 

class TaskOne: public ACE_Task<ACE_MT_SYNCH>

{

public:

//Implement the Service Initialization and Termination methods

int open(void*)

{

ACE_DEBUG((LM_DEBUG,"(%t) Active Object opened /n"));

 

//Activate the object with a thread in it.

activate();

 

return 0;

}

 

int close(u_long)

{

ACE_DEBUG((LM_DEBUG, "(%t) Active Object being closed down /n"));

return 0;

}

 

int svc(void)

{

ACE_DEBUG((LM_DEBUG,

"(%t) This is being done in a separate thread /n"));

 

// do thread specific work here

//.......

//.......

return 0;

}

};

 

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

{

//Create the task

TaskOne *one=new TaskOne;

 

//Start up the task

one->open(0);

 

//wait for all the tasks to exit

ACE_Thread_Manager::instance()->wait();

ACE_DEBUG((LM_DEBUG,"(%t) Main Task ends /n"));

}

 

上面的例子演示怎樣把

 

ACE_Task當作更高級的線程來使用。在例子中,TaskOne類派生自ACE_Task,並實現了open()close()svc()方法。在此任務對象實例化後,程序就調用它的open()方法。該方法依次調用activate()方法,致使一個新線程被派生和啓動。該線程的入口是svc()

方法。主線程等待主動對象線程終止,然後就退出進程。

 

5.2.3 任務間通信

  如前面所提到的,

 

ACE中的每個任務都有一個底層消息隊列(參見上面的圖示)。這個消息隊列被用作任務間通信的一種方法。當一個任務想要與另一任務“談話”時,它創建一個消息,並將此消息放入它想要與之談話的任務的消息隊列。接收任務通常用getq()

從消息隊列裏獲取消息。如果隊列中沒有數據可用,它就進入休眠狀態。如果有其他任務將消息插入它的隊列,它就會甦醒過來,從隊列中拾取數據並處理它。因而,在這種情況下,接收任務將從發送任務那裏接收消息,並以應用特定的方式作出反饋。

  下一個例子演示兩個任務怎樣使用它們的底層消息隊列進行通信。這個例子包含了經典的生產者-消費者問題的實現。生產者任務生成數據,將它發送給消費者任務。消費者任務隨後消費這個數據。使用

 

ACE_Task構造,我們可將生產者和消費者看作是不同的ACE_Task

類型的對象。這兩種任務使用底層消息隊列進行通信。

 

 

5-2

#include "ace/OS.h"

#include "ace/Task.h"

#include "ace/Message_Block.h"

 

//The Consumer Task.

class Consumer:

public ACE_Task<ACE_MT_SYNCH>

{

public:

int open(void*)

{

ACE_DEBUG((LM_DEBUG, "(%t) Producer task opened /n"));

 

//Activate the Task

activate(THR_NEW_LWP,1);

 

return 0;

}

 

//The Service Processing routine

int svc(void)

{

//Get ready to receive message from Producer

ACE_Message_Block * mb =0;

do

{

mb=0;

 

//Get message from underlying queue

getq(mb);

ACE_DEBUG((LM_DEBUG,

"(%t)Got message: %d from remote task/n",*mb->rd_ptr()));

}while(*mb->rd_ptr()<10);

 

return 0;

}

 

int close(u_long)

{

ACE_DEBUG((LM_DEBUG,"Consumer closes down /n"));

return 0;

}

};

 

class Producer:

public ACE_Task<ACE_MT_SYNCH>

{

public:

Producer(Consumer * consumer):

consumer_(consumer), data_(0)

{

mb_=new ACE_Message_Block((char*)&data_,sizeof(data_));

}

 

int open(void*)

{

ACE_DEBUG((LM_DEBUG, "(%t) Producer task opened /n"));

 

//Activate the Task

activate(THR_NEW_LWP,1);

return 0;

}

 

//The Service Processing routine

int svc(void)

{

while(data_<11)

{

//Send message to consumer

ACE_DEBUG((LM_DEBUG,

"(%t)Sending message: %d to remote task/n",data_));

consumer_->putq(mb_);

 

//Go to sleep for a sec.

ACE_OS::sleep(1);

data_++;

}

 

return 0;

}

 

int close(u_long)

{

ACE_DEBUG((LM_DEBUG,"Producer closes down /n"));

return 0;

}

 

private:

char data_;

Consumer * consumer_;

ACE_Message_Block * mb_;

};

 

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

{

Consumer *consumer = new Consumer;

Producer * producer = new Producer(consumer);

 

producer->open(0);

consumer->open(0);

 

//Wait for all the tasks to exit. ACE_Thread_Manager::instance()->wait();

}

 

在此例中,生產者和消費者任務非常相似。它們都沒有任何服務初始化或是終止代碼。但兩個類的

 

svc()方法是不同的。生產者在open()方法中被啓用後,svc()方法會被調用。在此方法中,生產者生成一個消息,將它插入消費者的隊列。消息是使用ACE_Message_Block類來生成的(要更多地瞭解如何使用ACE_Message_Block,請閱讀此教程及在線ACE指南中有關消息隊列的章節)。生產者維護指向消費者任務(對象)的指針。它通過該指針來將消息放入消費者的消息隊列中。該指針在main()

函數中通過生產者的構造器設置。

  消費者駐留在它的

 

svc()方法的循環中,等待數據到達它的消息隊列。如果隊列中沒有數據,消費者就會阻塞並休眠(這是由ACE_Task

類魔術般地自動完成的)。一旦數據到達消費者的隊列,它就會甦醒並消費此數據。

  在此例中,生產者發送的數據由一個整數組成。生產者每次將這個整數加一,然後發送給消費者。

  如你所看到的,生產者-消費者問題的解決方案十分簡單,並且是面向對象的。在編寫面向對象的多線程程序時,使用

 

ACE_Task是比使用低級線程API

更好的方法。

 

5.3

主動對象模式Active Object Pattern

  主動對象模式用於降低方法執行和方法調用之間的耦合。該模式描述了另外一種更爲透明的任務間通信方法。

  該模式使用

 

ACE_Task類作爲主動對象。在這個對象上調用方法時,它就像是常規對象一樣。就是說,方法調用是通過同樣的->操作符來完成的,其不同在於這些方法的執行發生於封裝在ACE_Task中的線程內。在使用被動或主動對象進行編程時,客戶程序看不到什麼區別,或僅僅是很小的區別。對於構架開發者來說,這是非常有用的,因爲開發者需要使構架客戶與構架的內部結構屏蔽開來。這樣構架用戶就不必去擔心線程、同步、會合點(rendezvous

),等等。

 

5.3.1 主動對象模式工作原理

  主動對象模式是

 

ACE

實現的較爲複雜的模式中的一個。該模式有如下參與者:

 

主動對象(基於 ACE_Task)。 ACE_Activation_Queue 若干 ACE_Method_Object (主動對象的每個方法都需要有一個方法對象)。 若干 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對象。於是客戶就可以在任何它喜歡的時候去嘗試從“期貨”(future)對象中獲取結果。如果客戶試圖在結果被設置之前從期貨對象中提取結果,客戶將會阻塞。如果客戶不希望阻塞,它可以通過使用ready()調用來輪詢(poll)期貨對象。如果結果已被設置,該方法返回1;否則就返回0ACE_Future對象基於“多態期貨”(polymorphic futures

)的概念。

  call()

 

方法的實現應該將返回的ACE_Future對象的內部值設置爲從調用實際的方法實現所獲得的結果(這個實際的方法實現在ACE_Task

中編寫)。

  下面的例子演示主動對象模式是怎樣實現的。在此例中,主動對象是一個“

 

Logger”(日誌記錄器)對象。Logger使用慢速的I/O系統來記錄發送給它的消息。因爲此I/O

系統很慢,我們不希望主應用任務的執行因爲相對來說並非緊急的日誌記錄而減慢。爲了防止此情況的發生,並且允許程序員像發出普通的方法調用那樣發出日誌調用,我們使用了主動對象模式。

  Logger

 

類的聲明如下所示:

 

 

5-3a

//The worker thread with which the client will interact

class Logger: public ACE_Task<ACE_MT_SYNCH>

{

public:

//Initialization and termination methods

Logger();

virtual ~Logger(void);

virtual int open (void *);

virtual int close (u_long flags = 0);

 

//The entry point for all threads created in the Logger

virtual int svc (void);

 

///////////////////////////////////////////////////////

//Methods which can be invoked by client asynchronously.

///////////////////////////////////////////////////////

 

//Log message

ACE_Future<u_long> logMsg(const char* msg);

 

//Return the name of the Task

ACE_Future<const char*> name (void);

 

///////////////////////////////////////////////////////

//Actual implementation methods for the Logger

///////////////////////////////////////////////////////

u_long logMsg_i(const char *msg);

const char * name_i();

 

private:

char *name_;

ACE_Activation_Queue activation_queue_;

};

 

如我們所看到的,

 

Logger主動對象派生自ACE_Task,並含有一個ACE_Activation_QueueLogger支持兩個異步方法:logMsg()name()。這兩個方法應該這樣來實現:當客戶調用它們時,它們實例化相應的方法對象類型,並將它放入任務的私有啓用隊列。這兩個方法的實際實現(也就是“真正地”完成所需工作的方法)是logMsg_i()name_i()

  下面的代碼段顯示我們所需的兩個方法對象的接口,分別針對

 

Logger

主動對象中的兩個異步方法。

 

 

5-3b

//Method Object which implements the logMsg() method of the active

//Logger active object class

class logMsg_MO: public ACE_Method_Object

{

public:

//Constructor which is passed a reference to the active object, the

//parameters for the method, and a reference to the future which

//contains the result.

logMsg_MO(Logger * logger, const char * msg,

ACE_Future<u_long> &future_result);

virtual ~logMsg_MO();

 

//The call() method will be called by the Logger Active Object

//class, once this method object is dequeued from the activation

//queue. This is implemented so that it does two things. First it

//must execute the actual implementation method (which is specified

//in the Logger class. Second, it must set the result it obtains from

//that call in the future object that it has returned to the client.

//Note that the method object always keeps a reference to the same

//future object that it returned to the client so that it can set the

//result value in it.

virtual int call (void);

 

private:

Logger * logger_;

const char* msg_;

ACE_Future<u_long> future_result_;

};

 

//Method Object which implements the name() method of the active Logger

//active object class

class name_MO: public ACE_Method_Object

{

public:

//Constructor which is passed a reference to the active object, the

//parameters for the method, and a reference to the future which

//contains the result.

name_MO(Logger * logger, ACE_Future<const char*> &future_result);

virtual ~name_MO();

 

//The call() method will be called by the Logger Active Object

//class, once this method object is dequeued from the activation

//queue. This is implemented so that it does two things. First it

//must execute the actual implementation method (which is specified

//in the Logger class. Second, it must set the result it obtains from

//that call in the future object that it has returned to the client.

//Note that the method object always keeps a reference to the same

//future object that it returned to the client so that it can set the

//result value in it.

virtual int call (void);

 

private:

Logger * logger_;

ACE_Future<const char*> future_result_;

};

 

每個方法對象都有一個構造器,用於爲方法調用創建“罩子”(

 

closure)。這意味着構造器通過將調用的參數和返回值作爲方法對象中的私有成員數據記錄下來,來確保它們被此對象“記住”。調用方法包含的代碼將對在Logger主動對象中定義的實際方法實現(也就是,logMsg_i()name_i()

)進行委託。

  下面的代碼段含有兩個方法對象的實現:

 

 

5-3c

//Implementation for the logMsg_MO method object.

//Constructor

logMsg_MO::logMsg_MO(Logger * logger, const char * msg, ACE_Future<u_long>

&future_result)

:logger_(logger), msg_(msg), future_result_(future_result)

{

ACE_DEBUG((LM_DEBUG, "(%t) logMsg invoked /n"));

}

 

//Destructor

logMsg_MO::~logMsg_MO()

{

ACE_DEBUG ((LM_DEBUG, "(%t) logMsg object deleted./n"));

}

 

//Invoke the logMsg() method

int logMsg_MO::call (void)

{

return this->future_result_.set (

this->logger_->logMsg_i (this->msg_));

}

 

//Implementation for the name_MO method object.

//Constructor

name_MO::name_MO(Logger * logger, ACE_Future<const char*> &future_result):

logger_(logger), future_result_(future_result)

{

ACE_DEBUG((LM_DEBUG, "(%t) name() invoked /n"));

}

 

//Destructor

name_MO::~name_MO()

{

ACE_DEBUG ((LM_DEBUG, "(%t) name object deleted./n"));

}

 

//Invoke the name() method

int name_MO::call (void)

{

return this->future_result_.set (this->logger_->name_i ());

}

 

這兩個方法對象的實現是相當直接的。如上面所解釋的,方法對象的構造器負責創建“罩子”(捕捉輸入參數和結果)。

 

call()方法調用實際的方法實現,隨後通過使用ACE_Future::set()

方法來在期貨對象中設置值。

  下面的代碼段顯示

 

Logger主動對象自己的實現。大多數代碼都在svc()方法中。程序在這個方法中從啓用隊列裏取出方法對象,並調用它們的call()

方法。

 

 

5-3d

//Constructor for the Logger

Logger::Logger()

{

this->name_= new char[sizeof("Worker")];

ACE_OS:strcpy(name_,"Worker");

}

 

//Destructor

Logger::~Logger(void)

{

delete this->name_;

}

 

//The open method where the active object is activated

int Logger::open (void *)

{

ACE_DEBUG ((LM_DEBUG, "(%t) Logger %s open/n", this->name_));

return this->activate (THR_NEW_LWP);

}

 

//Called then the Logger task is destroyed.

int Logger::close (u_long flags = 0)

{

ACE_DEBUG((LM_DEBUG, "Closing Logger /n"));

return 0;

}

 

//The svc() method is the starting point for the thread created in the

//Logger active object. The thread created will run in an infinite loop

//waiting for method objects to be enqueued on the private activation

//queue. Once a method object is inserted onto the activation queue the

//thread wakes up, dequeues the method object and then invokes the

//call() method on the method object it just dequeued. If there are no

//method objects on the activation queue, the task blocks and falls

//asleep.

int Logger::svc (void)

{

while(1)

{

// Dequeue the next method object (we use an auto pointer in

// case an exception is thrown in the <call>).

auto_ptr<ACE_Method_Object> mo

(this->activation_queue_.dequeue ());

ACE_DEBUG ((LM_DEBUG, "(%t) calling method object/n"));

 

// Call it.

if (mo->call () == -1)

break;

 

// Destructor automatically deletes it.

}

 

return 0;

}

 

//////////////////////////////////////////////////////////////

//Methods which are invoked by client and execute asynchronously.

//////////////////////////////////////////////////////////////

//Log this message

ACE_Future<u_long> Logger::logMsg(const char* msg)

{

ACE_Future<u_long> resultant_future;

 

//Create and enqueue method object onto the activation queue

this->activation_queue_.enqueue

(new logMsg_MO(this,msg,resultant_future));

 

return resultant_future;

}

 

//Return the name of the Task

ACE_Future<const char*> Logger::name (void)

{

ACE_Future<const char*> resultant_future;

 

//Create and enqueue onto the activation queue

this->activation_queue_.enqueue

(new name_MO(this, resultant_future));

 

return resultant_future;

}

 

///////////////////////////////////////////////////////

//Actual implementation methods for the Logger

///////////////////////////////////////////////////////

u_long Logger::logMsg_i(const char *msg)

{

ACE_DEBUG((LM_DEBUG,"Logged: %s/n",msg));

 

//Go to sleep for a while to simulate slow I/O device

ACE_OS::sleep(2);

 

return 10;

}

 

const char * Logger::name_i()

{

//Go to sleep for a while to simulate slow I/O device

ACE_OS::sleep(2);

return name_;

 

  最後的代碼段演示應用代碼,它實例化

 

Logger

主動對象,並用它來進行日誌記錄:

 

 

5-3e

//Client or application code.

int main (int, char *[])

{

//Create a new instance of the Logger task

Logger *logger = new Logger;

 

//The Futures or IOUs for the calls that are made to the logger.

ACE_Future<u_long> logresult;

ACE_Future<const char *> name;

 

//Activate the logger

logger->open(0);

 

//Log a few messages on the logger

for (size_t i = 0; i < n_loops; i++)

{

char *msg= new char[50];

ACE_DEBUG ((LM_DEBUG,

Issuing a non-blocking logging call/n"));

ACE_OS::sprintf(msg, "This is iteration %d", i);

logresult= logger->logMsg(msg);

 

//Don’t use the log result here as it isn't that important...

}

 

ACE_DEBUG((LM_DEBUG,

"(%t)Invoked all the log calls /

and can now continue with other work /n"));

 

//Do some work over here...

// ...

// ...

 

//Find out the name of the logging task

name = logger->name ();

 

//Check to "see" if the result of the name() call is available

if(name.ready())

ACE_DEBUG((LM_DEBUG,"Name is ready! /n"));

else

ACE_DEBUG((LM_DEBUG,

"Blocking till I get the result of that call /n"));

 

//obtain the underlying result from the future object.

const char* task_name;

name.get(task_name);

ACE_DEBUG ((LM_DEBUG,

"(%t)==> The name of the task is: %s/n/n/n", task_name));

 

//Wait for all threads to exit.

ACE_Thread_Manager::instance()->wait();

}

 

客戶代碼在

 

Logger主動對象上發出若干非阻塞式異步調用。注意這些調用看起來就像是在針對常規被動對象一樣。事實上,調用是在另一個單獨的線程控制裏執行。在發出調用記錄多個消息後,客戶發出調用來確定任務的名字。該調用返回一個期貨給客戶。於是客戶就開始使用ready()方法去檢查結果是否已在期貨對象中設置。然後它使用get()方法去確定期貨的底層值。注意客戶的代碼是何等的優雅,沒有用到線程、同步,等等。所以,主動對象模式可以使你更加容易地編寫你的客戶代碼。 如上面所提到的,在主動對象被啓用後,各個新線程在
發佈了9 篇原創文章 · 獲贊 2 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章