有效使用反應堆Reactor的設計準則

轉摘

有效使用反應堆Reactor的設計準則
 
  ACE_Reactor是事件多路分離和事件處理器分派的強大構架。但是,像其他構架一樣,學習使用ACE_Reactor需要時間和努力。縮短學習曲線的的一種途徑是去理解有效使用反應堆所必須遵從的設計準則。下面描述的設計準則基於幫助ACE用戶正確進行反應堆構架編程所獲得的大量經驗。
 
7.5.1 理解具體事件處理器的返回值語義
 
具體事件處理器定義的各種handle_* 掛鉤方法的返回值致使ACE_Reactor以不同的方式工作。使用返回值來觸發不同行爲意在降低ACE_Reactor的API的複雜度。但是,返回值常常使得程序員莫明其妙。因而,理解從handle_* 方法返回的值的效應非常重要;這些值分爲三種情況:
 
零:handle_* 方法返回零(0)通知ACE_Reactor、事件處理器希望繼續像前面一樣被處理,也就是,它應該保持在ACE_Reactor的實現的一張表中。這樣,當下一次ACE_Reactor的事件多路分離器系統調用經由handle_events被調用時,它還會繼續包括該事件處理器的句柄。對於那些生存期超出一次handle_* 方法分派的事件處理器,這是一種“正常的”行爲。
 
大於零:handle_* 方法返回大於0(> 0)的值通知ACE_Reactor、事件處理器希望在ACE_Reactor阻塞在它的事件多路分離器系統調用上面之前,再一次被分派。對協作的事件處理器來說,這種特性有助於增強全面的系統“公正性”。特別地,這種特性使得一個事件處理器在再次持有控制之前,允許其他事件處理器被分派。
 
小於零:handle_* 方法返回小於0(< 0 )的值通知ACE_Reactor、事件處理器想要被關閉、並從ACE_Reactor的內部表中移除。爲完成此工作、ACE_Reactor調用事件處理器的handle_close清掃方法。該方法可以執行用戶定義的終止活動,比如刪除對象分配的動態內存或關閉日誌文件。handle_close方法返回後,ACE_Reactor將相關聯的具體事件處理器從它的內部表中移除。
 
爲減少handle_* 返回值所帶來的問題,在實現具體事件處理器時,遵守下面的設計準則:
 
設計準則0:不要手工刪除事件處理器對象或顯式調用handle_close棗相反,確保ACE_Reactor自動調用handle_close清掃方法。因而,應用必須遵從適當的協議來移除事件處理器,也就是,或者通過(1)從handle_* 掛鉤方法中返回負值,或者通過(2)調用remove_handler。
該設計準則確保ACE_Reactor能夠適當地清掃它的內部表。如果不服從這一準則,當ACE_Reactor試圖移除已經在外部被刪除的具體事件處理器時,就會帶來不可預測的內存管理問題。後面的設計準則詳細說明怎樣確保ACE_Reactor調用handle_close清掃方法。
 
設計準則1:從繼承自ACE_Event_Handler的類的handle_* 方法中返回的表達式必須是常量(constant)。這一設計準則有助於靜態地檢查是否handle_* 方法返回了恰當的值。如果必須違反此準則,開發者必須在return語句之前加一註釋,解釋爲何要使用變量,而不是常量。
 
設計準則2:如果從繼承自ACE_Event_Handler的類的handle_* 方法中返回的值不爲0,必須在return語句之前加一註釋,說明該返回值的含義。這一設計準則確保所有非0的返回值都是開發者有意使用的。

7.5.2 理解handle_close()清掃掛鉤的語義
 
必須記住handle_close清掃掛鉤方法只能由ACE_Reactor(1)隱式地調用,也就是,當handle_* 方法返回-1這樣的負值時,或是(2)顯式地調用,也就是,如果應用調用remove_handler方法來解除具體事件處理器的登記。特別地,ACE_Reactor不會在本地應用或是遠地應用關閉I/O句柄時自動調用handle_close。因此,應用必須確定何時I/O句柄已被關閉,並採取適當的步驟,以使ACE_Reactor觸發handle_close清掃方法。
下面的來自7.4.2.2的Logging_Handler代碼片段演示怎樣正確地觸發清掃掛鉤:
 
// Hook method for handling the reception of
// remote logging transmissions from clients.
int Logging_Handler::handle_input (ACE_HANDLE)
{
ssize_t n = peer_stream_.recv (&len, sizeof len);
 
if (n == 0)
// Trigger handle_close().
return -1;
 
//..
 
// Keep handler registered for ‘‘normal’’ case.
return 0;
}
 
當handle_input方法從recv那裏收到0,它就返回-1。該值觸發ACE_Reactor調用handle_close清掃掛鉤。
爲最少化handle_* 返回值所帶來的問題,在實現具體事件處理器時,應遵守下面的設計準則:
 
設計準則3:當你想要觸發具體事件處理器的相應handle_close清掃方法時,從handle_* 方法中返回一個負值。值-1通常用於觸發清掃掛鉤,因爲它是ACE_OS系統調用包裝中一個常用的錯誤代碼。但是,任何來自handle_* 方法的負數都將觸發handle_close。
 
設計準則4:將所有Event_Handler清掃活動限制在handle_close清掃方法中。一般而言,將所有的清掃活動合併到handle_close方法中,而不是分散在事件處理器的各個handle_* 方法中要更爲容易。在處理動態分配的、必須用delete this來清除的事件處理器時,特別需要遵從此設計準則(見準則9)。
 
7.5.3 記住ACE_Time_Value參數是相對的
 
傳遞給ACE_Reactor的schedule_timer方法的兩個ACE_Time_Value參數必須相對於當前時間指定。例如,下面的代碼調度一個對象,延遲delay秒後開始,每interval秒打印一次可執行程序的名字(也就是,argv[0]):
 
class Hello_World : public ACE_Event_Handler
{
public:
virtual int handle_timeout (const ACE_Time_Value &tv, const void *act)
{
ACE_DEBUG ((LM_DEBUG,
"%[s] %d, %d/n",
act,
tv.sec (),
tv.usec ()));
return 0;
}
};
 
int main (int argc, char *argv[])
{
if (argc != 3)
ACE_ERROR_RETURN ((LM_ERROR,
"usage: %s delay interval/n",
argv[0]), -1);
 
Hello_World handler; // timer object.
 
ACE_Time_Value delay = ACE_OS::atoi (argv[1]);
ACE_Time_Value interval = ACE_OS::atoi (argv[2]);
 
// Schedule the timer.
ACE_Reactor::instance ()->schedule_timer
(&handler,
(const void *) argv[0],
delay,
interval);
 
// Run the event loop.
for (;;)
ACE_Reactor::instance ()->handle_events ();
 
/* NOTREACHED */
}
 
一種常見的錯誤是誤將絕對的時間值傳遞給schedule_timer。例如,考慮一個不同的例子:
 
ACE_Time_Value delay = ACE_OS::atoi (argv[1]);
delay += ACE_OS::gettimeofday ();
 
// Callback every following 10 seconds.
ACE_Time_Value interval = delay + 10;
 
ACE_Reactor::instance ()->schedule_timer
(&handler,
0,
delay,
interval);
 
但是,該定時器在將來很長的時間內都不會到期,因爲它將本日的當前時間加到了用戶所要求的delay和interval上。
下面是實現具體事件處理器時,爲最小化與絕對的ACE_Time_Value有關的問題,所應遵從的設計準則:
 
設計準則5:不要將絕對時間用作ACE_Reactor::schedule_timer的第三或第四參數。一般而言,這些參數應該小於一個極長的延遲,更遠小於當前時間。
 
7.5.4 小心追蹤ACE_Event_Handler的生存期
 
對登記到ACE_Reactor上的ACE_Event_Handler的跟蹤失敗會導致各種問題。不使用像Purify[24]這樣的內存錯誤檢測工具,很難去追蹤這些問題;但這樣的工具也只能捕捉下面的一些、而不是全部的與生存期相關的問題:
 
7.5.4.1 保守地使用非動態分配的事件處理器
 
考慮下面的具體事件處理器的定義:
 
class My_Event_Handler : public ACE_Event_Handler
{
public:
My_Event_Handler (const char *str = "hello")
: str_ (ACE_OS::strnew (str)) {}
 
virtual int handle_close
(ACE_HANDLE = ACE_INVALID_HANDLE,
ACE_Reactor_Mask = ACE_Event_Handler::READ_MASK)
{
// Commit suicide.
delete this;
}
 
~My_Event_Handler (void)
{
delete [] this->str_;
}
 
// ...
 
private:
char *str_;
};
 
該類在從ACE_Reactor上移除時,通過它的handle_close清掃方法刪除它自己。儘管這看起來有一點不太傳統,它卻是完全有效的C++習語。但是,它僅在正被刪除的對象是動態分配的的情況下才能夠工作。
相反,如果正被刪除的對象不是動態分配的,全局動態內存堆將會被破壞。原因是delete操作符將把this解釋爲堆中有效的地址。當delete操作符試圖將非堆的內存插入它的內部空閒表時,就會造成微妙的內存管理問題。
下面的例子演示一個常見的可導致堆崩潰的使用實例:
 
int main (void)
{
// Non-dynamically allocated.
My_Event_Handler my_event_handler;
 
ACE_Reactor::instance ()->register_handler
(&my_event_handler,
ACE_Event_Handler::READ_MASK);
 
// ...
// Run event-loop.
while (/* ...event loop not finished... */)
ACE_Reactor::instance ()->handle_events ();
 
// The <handle_close> method deletes an
// object that wasn’t allocated dynamically...
ACE_Reactor::instance ()->remove_handler
(&my_event_handler,
ACE_Event_Handler::READ_MASK);
 
return 0;
}
 
上面代碼的問題是remove_handler被調用時,ACE_Reactor將會調用My_Event_Handler的handle_close方法。遺憾的是,handle_close方法會對my_event_handler對象執行delete this操作,而此對象並非是動態分配的。
防止發生此問題的一種方法是將析構器放置在My_Event_Handler的私有區域,也就是:
 
class My_Event_Handler : public ACE_Event_Handler
{
public:
My_Event_Handler (const char *str);
// ...
 
private:
// Place destructor into the private section
// to ensure dynamic allocation.
?My_Event_Handler (void);
// ...
};
 
在此類中,My_Event_Handler的析構器被放置在類的私有訪問控制區中。這種C++習語確保該類的所有實例都必須是動態分配的。如果實例被偶然地定義爲static或auto,它在編譯時就會被作爲錯誤標記出來。
下面是實現具體事件處理器時,爲最小化與具體事件處理器的生存期相關的問題,所應遵從的設計準則:
 
設計準則6:不要delete不是動態分配的事件處理器。任何含有delete this、而其類又沒有私有析構器的handle_close方法,都有可能違反這一設計準則。在缺乏一種能夠靜態地識別這一情況的規約檢查器時,應該在delete this的緊前面加上註釋,解釋爲何要使用這一習語。
 
7.5.4.2 適當地解除具體事件處理器的登記
 
下面的程序演示與具體事件處理器的生存期相關的另一種常見錯誤:
 
ACE_Reactor reactor;
 
int main (void)
{
My_Event_Handler my_event_handler;
 
ACE_Reactor::instance ()->register_handler
(&my_event_handler,
ACE_Event_Handler::READ_MASK);
 
while (/* ...event loop not finished... */)
ACE_Reactor::instance ()->handle_events ();
 
// The destructor of the ACE_Reactor singleton
// will be called when the process exits. It
// removes all registered event handlers.
return 0;
}
 
my_event_handler 的生存期由main函數的生存期決定。相反,ACE_Reactor單體的生存期由進程的生存期決定。因而,當進程退出時,反應堆的析構器將會被調用。通過調用所有仍然登記在冊的事件處理器的handle_close方法,ACE_Reactor的析構器將這些處理器全部移除掉。但是,如果 my_event_handler仍然登記在Reactor上,它的handle_close方法將會在該對象出了作用域、並被銷燬之後調用。
下面是實現具體事件處理器時,爲最小化與具體事件處理器的生存期相關的問題,所應遵從的其他三條設計準則:
 
設計準則7:總是從堆中動態分配具體事件處理器。這是解決許多與具體處理器的生存期有關的問題的相對直接的方法。如果不可能遵從此準則,必須在具體事件處理器登記到ACE_Reactor時給出註釋,解釋爲什麼不使用動態分配。該註釋應該在將靜態分配的具體處理器登記到ACE_Reactor的register_handler語句的緊前面出現。
 
設計準則8:在ACE_Event_Handler退出它們“生活”的作用域之前,從與它們相關聯的ACE_Reactor中將它們移除掉。該準則應在未遵從準則7的情況下使用。
 
設計準則9:只允許在handle_close方法中使用delete this習語,也就是,不允許在其他handle_* 方法中使用delete this。該準則有助於檢查是否有與刪除非動態分配的內存有關的潛在錯誤。自然,與ACE_Reactor無關的組件可以擁有不同的對自刪除進行管轄的準則。
 
設計準則10:僅在爲具體事件處理器所登記的最後一個事件已從ACE_Reactor中移除時執行delete this操作。過早刪除在ACE_Reactor上登記了多個事件的具體處理器會導致“晃盪的指針”,遵從此準則可以避免發生這樣的情況。
例如,my_event_handler可以登記READ和WRITE事件,如下所示:
 
ACE_Reactor::instance ()->register_handler
(&my_event_handler,
ACE_Event_Handler::READ_MASK
| ACE_Event_Handler::WRITE_MASK);
 
  在此情形下,當handle_input返回-1時,ACE_Reactor將調用handle_close清掃掛鉤方法。在具體事件處理器登記的WRITE_MASK也被移除之前(例如,讓它返回一個負值,或是通過下面的語句顯式地將它移除),該方法不能執行delete this操作。
 
ACE_Reactor::instance ()->remove_handler
(&my_event_handler,
ACE_Event_Handler::WRITE_MASK);
 
下面的方法演示追蹤此信息的一種途徑:
 
class My_Event_Handler : public ACE_Event_Handler
{
public:
My_Event_Handler (void)
{
// Keep track of which bits are enabled.
ACE_SET_BITS (this->mask_,
ACE_Event_Handler::READ_MASK
| ACE_Event_Handler::WRITE_MASK);
 
// Register ourselves with the Reactor for
// both READ and WRITE events.
ACE_Reactor::instance ()->register_handler
(this, this->mask_);
}
 
virtual int handle_close (ACE_HANDLE h, ACE_Reactor_Mask mask)
{
if (mask == ACE_Event_Handler::READ_MASK)
{
ACE_CLR_BITS (this->mask_,
ACE_Event_Handler::READ_MASK);
 
// Perform READ_MASK cleanup logic.
}
else if (mask == ACE_Event_Handler::WRITE_MASK)
{
ACE_CLR_BITS (this->mask_,
ACE_Event_Handler::WRITE_MASK);
 
// Perform WRITE_MASK cleanup logic.
}
 
// Only delete ourselves if we’ve been closed
// down for both READ and WRITE events.
if (this->mask_ == 0)
delete this;
}
 
// ... handle_input() and handle_output() methods.
 
private:
ACE_Reactor_Mask mask_;
// Keep track of when to delete this.
};
 
上面的解決方案維護ACE_Reactor_Mask,追蹤何時一個具體事件處理器登記的所有事件已被從ACE_Reactor移除。
 
7.5.5 注意WRITE_MASK語義
 
  下面的代碼指示ACE_Reactor,只要可以無阻塞地向一個句柄寫,就回調一個event_handler。
 
ACE_Reactor::instance ()->mask_ops
(event_handler,
ACE_Event_Handler::WRITE_MASK,
ACE_Reactor::ADD_MASK);
 
但是,除非連接被流控制,否則總是可以向一個句柄寫。因此,反應堆會持續地回調event_handler的handle_output方法,直到(1)發生連接流控制或(2) mask_ops方法被指示清除WRITE_MASK。一種常見的編程錯誤是忘記清除此掩碼,導致ACE_Reactor不斷地調用 handle_output方法。應遵從下面的設計準則來避免這一問題:
 
設計準則11:當你不再需要具體事件處理器的handle_output方法被回調時,清除WRITE_MASK。
  下面的代碼演示怎樣確保handle_output方法不再被回調:
 
ACE_Reactor::instance ()->mask_ops
(event_handler,
ACE_Event_Handler::WRITE_MASK,
ACE_Reactor::CLR_MASK);
 
ACE_Reactor還定義了完成同樣操作的簡捷方法:
 
ACE_Reactor::instance ()->cancel_wakeup
(event_handler,
ACE_Event_Handler::WRITE_MASK);
 
這些方法通常在已不再有在具體事件處理器上待決的輸出消息時被調用。
 
爲幫助自動查驗此準則,程序員必須在他們的handle_output方法中插入註釋,這些註釋指示哪些返回路徑不會清除WRITE_MASK,也就是,事件處理器想要在“可以寫”時繼續被回調。同樣地,程序員還應該註釋那些WRITE_MASK被清除的路徑。如果在handle_output方法中沒有路徑清除WRITE_MASK,那就意味着可能違反了此準則。
例如,下面的handle_output方法演示此設計準則的可能的應用:
 
int My_Event_Handler::handle_output (ACE_HANDLE)
{
if (/* output queue is now empty */)
{
ACE_Reactor::instance ()->cancel_wakeup
(event_handler,
ACE_Event_Handler::WRITE_MASK);
/* Removing WRITE_MASK */
return 0;
} else
{
// ... continue to transmit messages
// from the output queue.
/* Not removing WRITE_MASK */
return 0;
}
}
 
如果沒有註釋指示對WRITE_MASK的清除,就有可能違反了此設計準則。
 
7.5.6 適當地登記具體事件處理器
 
當爲I/O操作在ACE_Reactor上登記具體事件處理器時,選擇下面的方法中的一種:
 
顯式地傳遞句柄:該方法使用下面的ACE_Reactor方法:
 
int register_handler
(ACE_HANDLE io_handle,
ACE_Event_Handler *event_handler,
ACE_Reactor_Mask mask);
 
並顯式地傳遞I/O設備的ACE_HANDLE,也就是:
 
void register_socket (ACE_HANDLE socket,
ACE_Event_Handler *handler)
{
ACE_Reactor::instance ()->register_handler
(socket,
handler,
ACE_Event_Handler::READ_MASK);
// ...
}
 
注意此register_handler方法允許同一個具體事件處理器與多個ACE_HANDLE一起進行登記。反應堆的這一特性使得我們有可能最小化處理許多客戶所需的狀態的數量;這些客戶同時與同一事件處理器相連接。
 
隱式地傳遞句柄:該方法使用ACE_Reactor的另一個register_handler方法:
 
int register_handler
(ACE_Event_Handler *event_handler,
ACE_Reactor_Mask mask);
 
在這種情形下,ACE_Reactor執行一次“雙重分派”(double-dispatch)[6]來通過具體事件處理器的get_handle方法從處理器中獲取底層的ACE_HANDLE。該方法在ACE_Event_Handler基類中定義,具有特徵const:
 
virtual ACE_HANDLE get_handle (void) const;
 
當使用隱式登記時,常見的一種錯誤是在從ACE_Event_Handler派生子類時忽略了get_handle上的const。這樣的疏忽將導致編譯器不能適當地在子類中重定義get_handle方法。相反,它將在子類中隱藏該方法,從而產生代碼、調用基類的get_handle方法;該方法缺省返回-1。
因此,服從下面的設計準則十分重要:
 
設計準則12:確定get_handle方法的特徵與ACE_Event_Handler基類中的一致。如果你不遵從此準則,並且你“隱式地”將ACE_HANDLE傳遞給ACE_Reactor,ACE_Event_Handler基類中的缺省get_handle將返回-1,而這是錯誤的。
 
7.5.7 從反應堆中移除已關閉的句柄/處理器
 
當連接被關閉時,句柄就不再能用於I/O。在這樣的情況下,select將會持續地報告句柄“讀就緒”,這樣你就可以在句柄上調用close了。此步驟通常在handle_close清掃方法中完成。
一個常見的錯誤是對已死句柄及其事件處理器的移除的失敗。這樣ACE_Reactor將會持續地回調事件處理器的handle_input方法,直到它被從ACE_Reactor中移除。下面的設計準則有助於避免這一問題:
 
設計準則13:當連接關閉時(或當連接上發生錯誤時),從handle_* 方法中返回一個負值。
 
遵從此設計準則的代碼通常被構造如下:
 
int handle_input (ACE_HANDLE handle)
{
// ...
ssize_t result = ACE_OS::read (handle, buf, bufsize);
 
if (result <= 0)
// Connection has closed down or an
// error has occurred.
return -1;
else
// ...
 
當返回-1時,ACE_Reactor將調用你的handle_close清掃方法。爲避免資源泄漏,確定該方法給了事件處理器以機會來刪除它自己,並關閉它的句柄(例如,ACE_OS::close (handle))。一旦handle_close返回,ACE_Reactor就有機會從它的內部表中移除句柄/處理器對。
 
7.5.8 使用DONT_CALL標誌來避免遞歸的handle_close()回調
 
前面的準則描述了在應用顯式或隱式地(通過從handle_* 掛鉤方法中返回負值)調用它的remove_handler時,ACE_Reactor怎樣自動調用handle_close方法。但是,如果應用在 handle_close清掃方法中調用remove_handler,就必須特別小心,因爲這有可能觸發無限遞歸。下面的準則處理這一問題。
 
設計準則14:在handle_close方法中調用remove_handler時,總是傳遞給它DONT_CALL標誌。該準則確保ACE_Reactor不會遞歸地調用handle_close方法。下面的代碼演示怎樣應用此準則:
 
int My_Event_Handler::handle_close
(ACE_HANDLE,
ACE_Reactor_Mask)
{
// ...
 
ACE_Reactor::instance ()->remove_handler
(this->get_handle (),
// Remove all the events for which we’re
// registered. We must pass the DONT_CALL
// flag here to avoid infinite recursion.
ACE_Event_Handler::RWE_MASK |
ACE_Event_Handler::DONT_CALL);
 
// ...
}
 
順便說一下,remove_handler通常在下列情況下在handle_close中被調用:(1)爲多個事件登記了同一個具體事件處理器,以及(2) handle_close第一次被調用時需要觸發事件處理器的完全關閉。因而,handle_close還應該移除在ACE_Reactor中與該事件處理器相關聯的其他事件。
 
7.6 結束語
 
ACE_Reactor是設計用於簡化併發的、事件驅動的分佈式應用的OO構架。通過在OO C++接口中封裝低級的OS事件多路分離機制,ACE_Reactor使得開發正確、簡潔、可移植和高效的應用變得更爲容易。同樣地,通過分離策略與機制,ACE_Reactor增強了複用、改善了可移植性,並提供了透明的可擴展性。
下面的C++語言特性對ACE_Reactor的設計和使用它的功能的應用有所幫助:
 
類:C++類提供的封裝改善了可移植性。例如,ACE_Reactor類將應用與像WaitForMultipleObjects和select這樣的OS事件多路分離器之間的差異屏蔽開來。
 
對象:將C++對象、而不是單獨的函數登記到ACE_Reactor有助於將應用特有的狀態和使用此狀態的方法集成在一起。
 
繼承和動態綁定:通過允許開發者不修改現有代碼就增強ACE_Reactor及與其相關聯的應用的功能,這些特性促進了透明的可擴展性。
 
模板:通過將可變性引入統一的類(它們可被“插入“到通用模板中),C++參數化類型有助於增強可複用性。例如,除了Logging_Handler和 ACE_SOCK_Acceptor,ACE_Acceptor還可用SVC_HANDLER和PEER_ACCEPTOR實例化。
 
使用ACE_Reactor的一個潛在的不利方面是一開始很難理解應用的主線程控制在哪裏執行。這是與事件循環回調分派器(比如ACE_Reactor或X -windows)相關的一個常見問題。但是,在使用此方法編寫若干應用後,圍繞這種“間接事件回調”分派模型的迷惑通常就會消失了。
ACE_Reactor和ACE socket包裝的C++源代碼和文檔可在http://www.cs.wustl.edu/~schmidt/ACE.html找到。這一版本還包括一套測試程序和例子,以及許多其他封裝命名管道、流管道、mmap和系統V IPC機制(也就是,消息隊列、共享內存和信號量)的C++包裝。
 
感謝 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章