ACE
的陷阱
坦白說(shuō),使用這個(gè)標(biāo)題無(wú)非是希望能夠吸引你的眼球,這篇文章的目的僅僅是為了揭示一些ACE缺陷的。文章適合的讀者是對(duì)ACE(ADAPTIVE Communication Environment)有一定研究,或者正在使用ACE從事項(xiàng)目開(kāi)發(fā)的人士參考。如果你對(duì)C++還是新手,甚至包括ACE知識(shí)初學(xué)者,(但你想飛的更高),建議你收藏這篇文檔以后閱讀。
秉承陷阱系列文章的傳統(tǒng),我只是通過(guò)一些辯證的角度去看ACE的一些不足,對(duì)于ACE的強(qiáng)大和優(yōu)美我就不再作贊美。從2000年,到現(xiàn)在,ACE在中國(guó)已經(jīng)從星星之火,開(kāi)始有燎原之勢(shì)。這一方面說(shuō)明ACE的優(yōu)美和實(shí)力已經(jīng)逐步得到大家的認(rèn)可(我所知道的Adobe reader的使用ACE,估計(jì)是為了跨平臺(tái),國(guó)內(nèi)的大量電信的網(wǎng)管,計(jì)費(fèi),智能網(wǎng)軟件也使用ACE),一方面要感謝的是的馬維達(dá)這位國(guó)內(nèi)少有的職業(yè)作家,國(guó)內(nèi)的ACE的中文資料(包括大量免費(fèi)資料)都出自這位老兄。
但ACE無(wú)疑是復(fù)雜的,能夠暢快的遨游在其中的絕對(duì)不是泛泛之輩。沒(méi)有對(duì)網(wǎng)絡(luò),設(shè)計(jì)模式,操作系統(tǒng)有一定的底蘊(yùn),想痛快的駕馭ACE無(wú)疑是較難的。另外,由于ACE仍然處在逐步發(fā)展的過(guò)程中。他的很多問(wèn)題仍然有待進(jìn)一步完善。重要的是一些文案的不足,受眾面狹小,導(dǎo)致許多ACE的使用者在使用ACE的時(shí)候會(huì)碰上很多問(wèn)題。這篇文案就是用于徹底揭示部分這些問(wèn)題。希望大家能在更加順捷的使用它。
另外,請(qǐng)注意我使用的陷阱這個(gè)術(shù)語(yǔ),而不是原罪。(C Trap and Pitfalls 倒有很多應(yīng)該是Original sin)ACE還在不停的發(fā)展中。很多問(wèn)題可能會(huì)在以后的版本中間改進(jìn)。所以在我認(rèn)為的的確是問(wèn)題的章節(jié)后面,我會(huì)附上知道錯(cuò)誤的版本號(hào)。
作為一個(gè)代碼級(jí)的中間件。ACE無(wú)疑是高效的,但是坦白說(shuō)ACE的代碼不是非常完美的。ACE的很多地方提供的是一個(gè)框架解決方案,為了保證框架的可移植和通用,代碼中大量使用了virtual 函數(shù),Bridge模式,多線程下的鎖操作,甚至有相當(dāng)?shù)?/span>new操作……,這些東西都限制ACE的性能。所以個(gè)人謹(jǐn)慎的將ACE的效率定義為中上。
個(gè)人認(rèn)為,一般情況下,如果你使用ACE的API代替系統(tǒng)API,速度應(yīng)該降低0.01%以下,主要導(dǎo)致這些差役在于ACE的再次封裝,而函數(shù)棧的調(diào)用成本應(yīng)該可以幾乎不計(jì)。ACE的優(yōu)勢(shì)在高性能的系統(tǒng)架構(gòu),而不是絕對(duì)的函數(shù)性能,如果你要再考慮在加入系統(tǒng)框架的其它功能呢,(舉一個(gè)例子,當(dāng)你想把定時(shí)器優(yōu)美的合入你的代碼時(shí)),ACE就有足夠的優(yōu)勢(shì)讓你選擇他。【注】
在此啰嗦一句,同樣也有很多人質(zhì)疑STL的性能。所有好的類(lèi)庫(kù)一樣,他帶來(lái)優(yōu)勢(shì)的同時(shí)也會(huì)有一定的遺憾,比如少量性能降低。但是如果說(shuō)他們的性能不好,那是無(wú)稽之談。(不信,把你認(rèn)為性能差的代碼給我寫(xiě)寫(xiě)看。)建議固步自封的程序員不要再干買(mǎi)櫝還珠的事情,先去讀讀那些優(yōu)美的代碼。
但是和所有的框架一樣,ACE也有不少的地方的地方是性能的暗礁,你最好繞開(kāi)。當(dāng)然一般而言ACE會(huì)提供多條道路,重要的是你能選擇正確。
ACE的有多個(gè)層次,側(cè)記缺陷這類(lèi)錯(cuò)誤往往出現(xiàn)在ACE的高階封裝中。同時(shí)由于ACE是一個(gè)跨平臺(tái)的中間件。所以為了平臺(tái)的兼容性,ACE做了很多折中和彌補(bǔ),有些是很漂亮的,但有些卻不是非常理想。
所有的代碼都是不完美的,特別是ACE這種要讓無(wú)數(shù)人在無(wú)數(shù)環(huán)境下使用的軟件。很多使用不便的問(wèn)題都是來(lái)自我個(gè)人的一些習(xí)慣,這些算是苛責(zé)了。
由于ACE的龐大性,很多時(shí)候大家會(huì)錯(cuò)誤的理解使用ACE的某些代碼實(shí)現(xiàn)某些特性。在此將寫(xiě)一些曾經(jīng)讓我們?cè)愿^的陰溝寫(xiě)出來(lái)。另一方面,ACE的文檔的某些介紹也存在含混,會(huì)誤導(dǎo)大家的理解,錯(cuò)誤的地方。
2 ACE的鏈接Link錯(cuò)誤
很多人在Windows使用ACE的時(shí)候往往會(huì)出現(xiàn)以下的Link錯(cuò)誤。
Why do I get errors while using 'TryEnterCriticalSection'?
\ace/OS.i(2384) : error C2039:
'TryEnterCriticalSection': is not a member of '`global namespace''
其實(shí)這個(gè)錯(cuò)誤不是由于ACE導(dǎo)致的,只是編譯器把這個(gè)贓栽倒了ACE上。出現(xiàn)這個(gè)錯(cuò)誤的原因主要是因?yàn)橐恍╆P(guān)鍵宏定義沖突,一般是_WIN32_WINNT,'TryEnterCriticalSection' 這個(gè)函數(shù)是NT4.0后才出現(xiàn)的函數(shù),如果這個(gè)宏被定義的小于0x0400或者沒(méi)有定義,那么就會(huì)出現(xiàn)這個(gè)錯(cuò)誤。
所以最簡(jiǎn)單的處理方法是在自己的預(yù)定義頭文件中加入一行。
#if !defined (_WIN32_WINNT)
# define _WIN32_WINNT 0x0400
#endif
其實(shí)ACE自己對(duì)于宏的處理是比較嚴(yán)謹(jǐn)?shù)模?/span>ACE的config-win32-common.h中間就有這行定義,所以在一般而言,可以將ACE的頭文件包含定義放在在頂部,這樣也可以避免這個(gè)編譯錯(cuò)誤。
預(yù)定義頭文件是一個(gè)良好的編程習(xí)慣,你可以將自己的大部分宏定義,include包含的本工程以外的外部.h文件。簡(jiǎn)言之就是預(yù)定義頭文件中使用#include<>,表示包含工程以外文件,自己工程內(nèi)部只使用#include””,表示包含當(dāng)前工程目錄下的文件。大部分C/C++的程序員都有過(guò)鏈接和一些預(yù)定義沖突錯(cuò)誤消耗大量的時(shí)間,原來(lái)我也是如此,但是在掌握預(yù)定義頭文件方法后,我?guī)缀鯖](méi)有為這個(gè)問(wèn)題折磨過(guò)。其實(shí)Virsual C++ 在生產(chǎn)MFC工程的時(shí)候,會(huì)自動(dòng)幫你自動(dòng)生產(chǎn)一個(gè)預(yù)定義頭文件stdafx.h,只是我們不善利用而已。
其實(shí)對(duì)于很多編譯器,使用預(yù)定義頭文件還可以加快編譯速度。Virusal C++的預(yù)定義會(huì)生產(chǎn)一個(gè)pch文件,基本可以提高編譯速度一倍。Virusal C++的工程中間有專(zhuān)門(mén)的預(yù)定義頭文件設(shè)置。C++ Builder采用可以采用的編譯宏(好像是專(zhuān)用的)加快編譯速度。大致的原理是編譯器會(huì)在對(duì)預(yù)定義頭文件中包含的文件進(jìn)行與處理,在外部文件沒(méi)有發(fā)生改動(dòng)的時(shí)候,編譯器可以使用編譯這些文件生成的中間文件加快編譯速度。
ACE有一個(gè)非常優(yōu)美的定時(shí)器隊(duì)列模型,他提供了4種定時(shí)器Queue讓大家使用:ACE_Timer_Heap,ACE_Timer_Wheel,ACE_High_Res_Timer,ACE_Timer_Hash。在《C++ Network Programming Volume 2 - Systematic Reuse with ACE and Frameworks》中間有相應(yīng)的說(shuō)明,其中按照說(shuō)明最誘人的的是:
ACE_Timer_Hash, which uses a hash table to manage the queue. Like the timing wheel implementation, the average-case time required to schedule, cancel, and expire timers is O(1) and its worst-case is O(n).
但是遺憾的是,ACE_Timer_Hash其實(shí)是性能最差的。幾乎不值得使用。我曾經(jīng)也被誘惑過(guò),但是在測(cè)試中間發(fā)現(xiàn),文檔中所述根本不屬實(shí),在一個(gè)大規(guī)模定時(shí)器的程序中,我使用ACE_Timer_Hash發(fā)現(xiàn)性能非常不理想,檢查后發(fā)現(xiàn)ACE的源代碼如下:
template <class TYPE, class FUNCTOR, class ACE_LOCK, class BUCKET> int
ACE_Timer_Hash_T<TYPE, FUNCTOR, ACE_LOCK, BUCKET>::expire (const ACE_Time_Value &cur_time)
{
// table_size_為Hash的桶尺寸,如果要避免沖突,桶的數(shù)量應(yīng)該盡量大,
//每個(gè)桶可以理解為一個(gè)Hash開(kāi)鏈的鏈表
// Go through the table and expire anything that can be expired
//遍歷所有的桶
for (size_t i = 0;
i < this->table_size_;
++i)
{
//在每個(gè)桶中檢查是否有要進(jìn)行超時(shí)處理的元素
while (!this->table_[i]->is_empty ()
&& this->table_[i]->earliest_time () <= cur_time)
{
…………
簡(jiǎn)單說(shuō)明一下上面的代碼,ACE_Timer_Hash_T采用開(kāi)鏈的Hash方式,每個(gè)桶就是一個(gè)鏈表,在超時(shí)檢查時(shí)所有的桶中是由有要進(jìn)行超時(shí)處理的元素。所以在超時(shí)處理中ACE采用了遍歷所有元素的方法。但悖論是如果你希望Hash的沖突不大,你就必須將桶的個(gè)數(shù)調(diào)整的盡量多。我在測(cè)試中將上述的程序的Time_Queue替換為標(biāo)準(zhǔn)的的ACE_Timer_Heap,發(fā)現(xiàn)性能提高數(shù)百倍。
冷靜下來(lái)思考一下,這也是正常的。對(duì)于一個(gè)Hash的實(shí)現(xiàn),保證查詢(xún)的速度,也就是通過(guò)定時(shí)器ID進(jìn)行操作的速度是足夠快的。但是實(shí)際上對(duì)于定時(shí)器操作,最大的成本應(yīng)該是尋找要超時(shí)的定時(shí)器,對(duì)于Hash這種數(shù)據(jù)結(jié)構(gòu),只能采用迭代遍歷的方式……, 所以采用Hash的低效是正常的。而原文應(yīng)該改為schedule, cancel,的最好時(shí)間復(fù)雜度是O(1),最差是O(n),而expire的時(shí)間復(fù)雜度始終是O(n)。
這個(gè)問(wèn)題至少倒5.6.1的版本還是存在的。我個(gè)人估計(jì)也不會(huì)得到解決。Hash的特性擺在那兒呢,除非ACE采用更加復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。
4 Reactor定時(shí)器的精度取決于實(shí)現(xiàn)
由于Reactor在各個(gè)平臺(tái)的默認(rèn)實(shí)現(xiàn)都取決于平臺(tái)的實(shí)現(xiàn),比如在Windows下默認(rèn)的Reactor是WFMO_REACTOR,而在Linux和UNIX平臺(tái),默認(rèn)的Reactor是Select_Reactor,而Reactor的實(shí)現(xiàn)往往取決于使用的反應(yīng)器底層實(shí)現(xiàn),而這些反應(yīng)器的時(shí)間精度就決定了你的定時(shí)器的時(shí)間精度。下表大致反饋了一些常用的定時(shí)器的實(shí)現(xiàn)。
表1 常用Raactor的實(shí)現(xiàn)
Reactor
|
反應(yīng)器的底層實(shí)現(xiàn)
|
時(shí)間精度
|
ACE_Select_Reactor
|
select函數(shù)
|
使用struct timeval結(jié)構(gòu)進(jìn)行超時(shí)處理; timeval 結(jié)構(gòu)可以精確倒微秒。
|
Dev_Poll_Reactor
|
poll或者而epoll
|
timeout參數(shù)的單位是毫秒。
|
ACE_WFMO_REACTOR
|
WaitForMultipleObjects
|
dwMilliseconds 的參數(shù)單位是毫秒
|
|
|
|
不過(guò)作為服務(wù)器的開(kāi)發(fā),我倒想不出什么地方需要精確到0.1s定時(shí)器的地方,了解一下差異性就足夠了。
WFMO_Reactor是ACE_Reactor在Windows下的默認(rèn)實(shí)現(xiàn)(為什么不選擇ACE_Select_Reactor作為默認(rèn)實(shí)現(xiàn),可能是基于效率和強(qiáng)大性的考慮),WFMO_Reactor的低層使用的函數(shù)是WaitForMultipleObjects和WSAEventSelect,WSAEnumNetworkEvents。其中WaitForMultipleObjects函數(shù)用于處理線程,互斥量,信號(hào)燈,事件,定時(shí)器等事件,而WSAEventSelect用于處理網(wǎng)絡(luò)IO事件。
由于Windows API和操作系統(tǒng)的特性不一樣,WFMO_Reactor在很多地方的表現(xiàn)和其他平臺(tái)不一致。 【注】
【注】其實(shí)這兩個(gè)問(wèn)題在《C++ Network Programming Volume 2 - Systematic Reuse with ACE and Frameworks》中4.4 The ACE_WFMO_Reactor Class有說(shuō)明。這兒算是借花獻(xiàn)佛。
由于WaitForMultipleObjects不是一個(gè)處理大量事件的函數(shù),其最多處理64個(gè)事件句柄,而WFMO_Reactor自身為了處理使用了2個(gè)句柄,所以一個(gè)WFMO_Rector對(duì)象只能處理。
如果你想做大規(guī)模的網(wǎng)絡(luò)接入,62個(gè)事件句柄顯然是不夠的,特別是要同時(shí)處理IO事件時(shí),導(dǎo)致這個(gè)不足的應(yīng)該是WFMO_Reactor的設(shè)計(jì)者的一個(gè)選擇。在賦予WFMO_Reactor強(qiáng)大的特性的同時(shí),WFMO_Reactor的設(shè)計(jì)者只能讓網(wǎng)絡(luò)IO事件的數(shù)量委屈一下了。
WFMO_Reactor 選擇的是Windows的WSAEventSelect 函數(shù)作為網(wǎng)絡(luò)的IO的反應(yīng)器。但是WSAEventSelect函數(shù)的FD_WRITE的事件處理和傳統(tǒng)的IO反應(yīng)器(select)不同。下面是MSDN的描述。
The FD_WRITE network event is handled slightly differently. An FD_WRITE network event is recorded when a socket is first connected with connect/WSAConnect or accepted with accept/WSAAccept, and then after a send fails with WSAEWOULDBLOCK and buffer space becomes available. Therefore, an application can assume that sends are possible starting from the first FD_WRITE network event setting and lasting until a send returns WSAEWOULDBLOCK. After such a failure, the application will find out that sends are again possible when an FD_WRITE network event is recorded and the associated event object is set.
簡(jiǎn)單翻譯就是,只有在三種條件下,WSAEventSelect才會(huì)發(fā)出FD_WRITE通知,一是使用connect或WSAConnect,一個(gè)套接字成功建立連接后;二是使用accept或WSAAccept,套接字被接受以后;三是若send、WSASend、sendto或WSASendTo函數(shù)返回失敗,而且錯(cuò)誤是WSAEWOULDBLOCK錯(cuò)誤后,緩沖區(qū)的空間再次變得可用時(shí)。【注】
【注】這種觸發(fā)方式在IO反應(yīng)器或者說(shuō)IO多路復(fù)用模型中應(yīng)該被稱(chēng)為邊緣觸發(fā)方式。select函數(shù)好像沒(méi)有這種觸發(fā)方式而是水平觸發(fā)方式, Epoll是支持這種方式的,但是默認(rèn)還是水平觸發(fā),這種方式可能有更高的效率,但是代碼更加難寫(xiě)。
可以這么理解,WSAEventSelect認(rèn)為套接字基本都是可寫(xiě)狀態(tài),它認(rèn)為你應(yīng)該大膽send。只有send出現(xiàn)WSAEWOULDBLOCK失敗后,你才需要使用WSAEventSelect反應(yīng)器。【注】
所以對(duì)于WFMO_Reactor的,你不可能依靠注冊(cè)(或者是喚醒)IO句柄進(jìn)行寫(xiě)操作,WMFO_Reactor很有可能不會(huì)去回調(diào)你的handle_output函數(shù)。
【注】對(duì)于網(wǎng)絡(luò)套接字,只要緩沖區(qū)還有空間就可以直接發(fā)送,除非緩沖區(qū)沒(méi)有空間了,才可能出現(xiàn)阻塞錯(cuò)誤,所以直接send失敗的可能性很小,另外反復(fù)調(diào)用注冊(cè)IO句柄一類(lèi)的操作其實(shí)是比較耗時(shí)的。其實(shí)先send,如果send失敗再注冊(cè)IO句柄到反應(yīng)器的方式應(yīng)該是一種更加高效的方式,高壓力的通訊服務(wù)器應(yīng)該選擇這個(gè)編寫(xiě)方式。
我自己的通信服務(wù)器通過(guò)這個(gè)改造,提高的性能在15%左右(CPU占用率下降)。
由于WFMO_Reactor的這些特點(diǎn),其實(shí)很大的限制了Reactor的可移植性。其實(shí)個(gè)人感覺(jué)如果你對(duì)系統(tǒng)特性沒(méi)有那么多要求,在Windows下選擇Select_Reactor替換WFMO_Reactor是更好的選擇。
6 盡量使用ID取消ACE_Event_Handler定時(shí)器
ACE的Reactor 提供了兩種方式取消定時(shí)器:
virtual int cancel_timer (ACE_Event_Handler *event_handler,
int dont_call_handle_close = 1);
virtual int cancel_timer (long timer_id,
const void **arg = 0,
int dont_call_handle_close = 1);
一種是使用定時(shí)器ID取消定時(shí)器,這個(gè)ID是定時(shí)器是的返回值,一種是采用相應(yīng)的ACE_Event_Handler指針取消定時(shí)器。一般情況下使用ACE_Event_Handler的指針取消定時(shí)器無(wú)疑是最簡(jiǎn)單的方法,但是這個(gè)方法卻不是一個(gè)高效的實(shí)現(xiàn)。所以如果您的程序有大規(guī)模的定時(shí)器設(shè)置取消操作,建議盡量使用ID取消定時(shí)器。我們用ACE_Timer_Heap和ACE_Timer_Has兩個(gè)Timer_Queue剖析一下。
先選擇最常用的Time_Queue ACE_Timer_Heap舉例,其使用ACE_Event_Handler關(guān)閉定時(shí)器的代碼是:
template <class TYPE, class FUNCTOR, class ACE_LOCK> int
ACE_Timer_Heap_T<TYPE, FUNCTOR, ACE_LOCK>::cancel (const TYPE &type,
int dont_call)
{
// Try to locate the ACE_Timer_Node that matches the timer_id.
//循環(huán)比較所有的的ACE_Event_Handler的指針是否相同
for (size_t i = 0; i < this->cur_size_; )
{
if (this->heap_[i]->get_type () == type)
{
………………
}
}
而使用TIMER_ID關(guān)閉的代碼如下,它是通過(guò)數(shù)組下標(biāo)進(jìn)行的定位操作。
template <class TYPE, class FUNCTOR, class ACE_LOCK> int
ACE_Timer_Heap_T<TYPE, FUNCTOR, ACE_LOCK>::cancel (long timer_id,
const void **act,
int dont_call)
{
//通過(guò)數(shù)組下標(biāo)操作,速度當(dāng)然奇快無(wú)比。
ssize_t timer_node_slot = this->timer_ids_[timer_id];
……
//跟進(jìn)數(shù)組ID進(jìn)行操作
else
{
ACE_Timer_Node_T<TYPE> *temp =
this->remove (timer_node_slot);
}
}
對(duì)于ACE_Timer_Heap,采用ACE_Event_Handler指針取消定時(shí)器的方式的平均時(shí)間復(fù)雜度應(yīng)該就是O(N)。由于ACE的的一個(gè)Event_handler可能對(duì)應(yīng)多個(gè)定時(shí)器,所以必須檢查所有的才能確保取消所有的相關(guān)定時(shí)器。
對(duì)于Timer_Hash,其通過(guò)ACE_Event_Handler關(guān)閉定時(shí)器的代碼是:
template <class TYPE, class FUNCTOR, class ACE_LOCK, class BUCKET> int
ACE_Timer_Hash_T<TYPE, FUNCTOR, ACE_LOCK, BUCKET>::cancel (const TYPE &type,
int dont_call)
{
Hash_Token<TYPE> **timer_ids = 0;
//根據(jù)Event Handler有一個(gè)定時(shí)器new一個(gè)數(shù)組出來(lái)
ACE_NEW_RETURN (timer_ids,
Hash_Token<TYPE> *[this->size_],
-1);
size_t pos = 0;
//根據(jù)定時(shí)器的個(gè)數(shù)再進(jìn)行取消
for (i = 0;
i < this->table_size_;
++i)
{
ACE_Timer_Queue_Iterator_T<TYPE,
ACE_Timer_Hash_Upcall<TYPE, FUNCTOR, ACE_LOCK>,
ACE_Null_Mutex> &iter =
this->table_[i]->iter ();
可以看到Timer_Hash的cancel比ACE_Timer_Heap的cancel(Event_Handler)要好一點(diǎn)點(diǎn)。但是其中也有new和delete操作,這些操作也不是高效操作。
所以說(shuō)在大規(guī)模的定時(shí)器使用中,推薦你還是使用定時(shí)器的ID取消定時(shí)器更加高效的多。
ACE_Pipe是一個(gè)跨平臺(tái)的管道實(shí)現(xiàn)。標(biāo)準(zhǔn)情況來(lái)講,采用的實(shí)現(xiàn),但是在最大的兩個(gè)平臺(tái)Windows和Linux上,ACE的實(shí)現(xiàn)是采用的Socket實(shí)現(xiàn)。
int
ACE_Pipe::open (int buffer_size)
{
ACE_TRACE ("ACE_Pipe::open");
#if defined (ACE_LACKS_SOCKETPAIR) || defined (__Lynx__)
//綁定了一個(gè)本地端口,0.0.0.0,然后找到相應(yīng)的端口,用于后面的鏈接
if (acceptor.open (local_any) == -1
|| acceptor.get_local_addr (my_addr) == -1)
result = -1;
else
{
// Establish a connection within the same process.
if (connector.connect (writer, sv_addr) == -1)
result = -1;
……
所以很多管道特性所特有的東西,在這兩個(gè)平臺(tái)上是無(wú)法使用ACE_Pipe實(shí)現(xiàn)的。比如,管道的特性可以保證在暫時(shí)沒(méi)有接受者的情況下使用,而Socket是不可能有這個(gè)特性的。你必須保證先有接受者,后有發(fā)送者的時(shí)序。
所以在這些平臺(tái)上最好不用這個(gè)封裝。
在Reactor的模式,有一種輔助的通知機(jī)制,Notify機(jī)制,簡(jiǎn)單說(shuō)就是通過(guò)通知發(fā)起者調(diào)用notify函數(shù),notify的消息被保存在一個(gè)管道中,handle_event的處理中會(huì)檢查這個(gè)管道中是否有通知數(shù)據(jù),如果有就根據(jù)通知的消息,會(huì)根據(jù)默認(rèn)的通知消息的類(lèi)型去調(diào)用hanle_input等函數(shù)。
從設(shè)計(jì)的角度將,這個(gè)機(jī)制無(wú)疑是非常優(yōu)美的,對(duì)于Reactor,它在IO驅(qū)動(dòng)以外,提供了一種新的驅(qū)動(dòng)方式。但是從實(shí)現(xiàn)角度來(lái)講,這個(gè)機(jī)制要慎用。原因有兩個(gè)。
ACE Reactor的默認(rèn)Notify方式采用的是ACE_Pipe,所以ACE_Pipe在Windows和Linux平臺(tái)上的問(wèn)題,Notify機(jī)制把ACE_Pipe的缺陷一個(gè)不少的繼承了,而且問(wèn)題更加多。
/**
* Contains the ACE_HANDLE the ACE_Dev_Poll_Reactor is listening
* on, as well as the ACE_HANDLE that threads wanting the attention
* of the ACE_Dev_Poll_Reactor will write to.
*/
ACE_Pipe notification_pipe_;
原來(lái)在調(diào)試ACE代碼的時(shí)候,我發(fā)現(xiàn)只要一使用Reactor,即使只使用定時(shí)器(除非明確不使用Notify),防火墻都會(huì)報(bào)警有監(jiān)聽(tīng)端口。我曾經(jīng)對(duì)此大惑不解,直到讀了ACE的這部分原代碼。這樣做的壞處有很多。第一個(gè)是由于采用的阻塞IO。速度會(huì)慢很多,第二個(gè)由于是單線程的處理,如果在壓力極大的情況下,可能出現(xiàn)死鎖的問(wèn)題。比如在有大規(guī)模的Notify的情況下,發(fā)送緩沖區(qū)很可能會(huì)被塞滿(mǎn)(由于是單線程,這時(shí)不會(huì)有接受者),同時(shí)由于為了簡(jiǎn)化,ACE_Pipe采用的IO是阻塞的,所以會(huì)導(dǎo)致整個(gè)程序死鎖。第三就是這樣的情況下ACE_Pipe會(huì)打開(kāi)一個(gè)臨時(shí)的端口,而且會(huì)綁定所有的IP(0.0.0.0),如果對(duì)于一個(gè)安全要求嚴(yán)格的的場(chǎng)景,這個(gè)將是一個(gè)不可饒恕的錯(cuò)誤。【注】
【注】在一個(gè)安全要求嚴(yán)格的環(huán)境下,這個(gè)臨時(shí)端口輕則可以讓你的服務(wù)器輕易陷于崩潰,重則可以讓你整個(gè)網(wǎng)絡(luò)被黑客攻陷。
不過(guò)還好的是ACE的開(kāi)發(fā)者估計(jì)自己也意識(shí)倒了這個(gè)麻煩。所以提供了另外一種消息隊(duì)列的方式。你可以通過(guò)定義ACE_HAS_REACTOR_NOTIFICATION_QUEUE的宏編譯ACE,這樣ACE將不使用ACE_Pipe作為Notify消息的管道,而使用一個(gè)自己的內(nèi)存隊(duì)列保存Notify消息,這個(gè)隊(duì)列是動(dòng)態(tài)擴(kuò)展的。而且由于是內(nèi)存操作,性能方面沒(méi)有太大問(wèn)題。
大體位置在重復(fù)編譯的衛(wèi)哨后面,#include /**/ "ace/pre.h"前面。保證這個(gè)宏起到作用。
#ifndef ACE_CONFIG_LINUX_H
#define ACE_CONFIG_LINUX_H
//使用內(nèi)存隊(duì)列作為Notify Queue
#define ACE_HAS_REACTOR_NOTIFICATION_QUEUE
#include /**/ "ace/pre.h"
這個(gè)問(wèn)題到5.6.1還是存在的,估計(jì)由于歷史的原因,在很長(zhǎng)一段時(shí)間也不會(huì)得到解決。
同上,這也應(yīng)該是一個(gè)BUG,Reactor Notify的代碼有考慮不周的地方。Notify機(jī)制的本質(zhì)是提供了一條消息隊(duì)列讓大家有方法調(diào)用Event_handler,但是存在一種可能,在你的通知消息在消息隊(duì)列的時(shí)候,Event_hanlder由于后面的處理可能已經(jīng)handle_close了。但是ACE的dispatch_notify卻沒(méi)有考慮倒這一點(diǎn)(或者說(shuō)考慮倒這一點(diǎn)也不好解決)。
ACE_Select_Reactor_Notify::dispatch_notify函數(shù)的代碼。
int
ACE_Select_Reactor_Notify::dispatch_notify (ACE_Notification_Buffer &buffer)
{
…………
ACE_Event_Handler *event_handler =
buffer.eh_;
bool const requires_reference_counting =
event_handler->reference_counting_policy ().value () ==
ACE_Event_Handler::Reference_Counting_Policy::ENABLED;
//如果此時(shí)這個(gè)ACE_Event_Handler已經(jīng)被handle_close了,你如何是好。。。。
switch (buffer.mask_)
{
case ACE_Event_Handler::READ_MASK:
case ACE_Event_Handler::ACCEPT_MASK:
result = event_handler->handle_input (ACE_INVALID_HANDLE);
這個(gè)bug到5.6.1還沒(méi)有解決。我覺(jué)得這個(gè)問(wèn)題是可以解決的(暫時(shí)還沒(méi)有提BUG),但是得到解決的方式卻仍然是低效的方案(還記得取消定時(shí)器的那個(gè)缺陷嗎)。
如果你仔細(xì)看過(guò)上面的幾節(jié),你也許會(huì)發(fā)出驚嘆,啊,又是Reactor Notify?對(duì),又是它。看起來(lái)我好像一直在和ACE的Notify機(jī)制在做對(duì),但它的確讓我吃了無(wú)數(shù)的苦頭。這部分的設(shè)計(jì)的確有一點(diǎn)畫(huà)蛇添足的感覺(jué),而且由于跨平臺(tái)性等原因,這個(gè)東東的實(shí)現(xiàn)一直不如意。其實(shí)自己使用ACE的實(shí)現(xiàn)(比如Message_Queue)一套這樣的機(jī)制應(yīng)該是易如反掌的事情。不苛求了。
如果你用不到Notify機(jī)制,最好在ACE_Reactor初始化的時(shí)候徹底關(guān)閉Notify機(jī)制。很多Reactor的初始化函數(shù)都提供了關(guān)閉notify pipe的方式。比如ACE_Select_Reactor_T的open函數(shù)的disable_notify_pipe參數(shù)。當(dāng)其為1的時(shí)候表示關(guān)閉notify 管道。
//disable_notify_pipe參數(shù)為1時(shí)表示關(guān)閉NOTIFY PIPE,不使用他
template <class ACE_SELECT_REACTOR_TOKEN> int
ACE_Select_Reactor_T<ACE_SELECT_REACTOR_TOKEN>::open
(size_t size,
int restart,
ACE_Sig_Handler *sh,
ACE_Timer_Queue *tq,
int disable_notify_pipe, /* 等于==1表示關(guān)閉notify機(jī)制 */
ACE_Reactor_Notify *notify)
不使用POLL和EPOLL【注】的人,估計(jì)不太知道這個(gè)ACE_Dev_Poll_Reactor,但實(shí)際上。特別是Linux下的EPOLL(一個(gè)IO多路服用模型),這是Linux大規(guī)模接入的重要法寶,從目前的表現(xiàn)來(lái)看,其他平臺(tái)上還沒(méi)有可以超越EPOLL的東西,Windows下的異步IO的性能也還遠(yuǎn)遠(yuǎn)遜于EPOLL。
如果要使用EPOLL而不是POLL,要使用宏ACE_HAS_EVENT_POLL編譯ACE,大體位置在重復(fù)編譯的衛(wèi)哨后面,#include /**/ "ace/pre.h"前面。保證起到作用。
#ifndef ACE_CONFIG_LINUX_H
#define ACE_CONFIG_LINUX_H
// ACE_HAS_EVENT_POLL宏用于定義使用EPOLL模塊,同時(shí)注意不同LINUX平臺(tái)下編譯可能有少量
//不同。我曾經(jīng)使用過(guò)的一個(gè)內(nèi)核2.4的Slackware平臺(tái),要在編譯ACE的時(shí)候加入 –lepoll,可能是由于
//其是打補(bǔ)丁增加的功能
#define ACE_HAS_EVENT_POLL
#include /**/ "ace/pre.h"
但也許是由于這個(gè)東西過(guò)新還是由于設(shè)計(jì)者是一個(gè)定于時(shí)間要求很敏感的人。的設(shè)計(jì)明顯的是定時(shí)器優(yōu)先。但是了解EPOLL和POLL的人都知道,UNIX和Linux設(shè)計(jì)這兩個(gè)咚咚的目的就是解決大規(guī)模IO復(fù)用。不是為了保證定時(shí)器優(yōu)先,所以我對(duì)這個(gè)設(shè)計(jì)很是不解,郁悶。其大體思路為,
1.) 先檢查定時(shí)器超時(shí)的隊(duì)列,計(jì)算最小的超時(shí)時(shí)間,用于IO等待。
2.) 觸發(fā)IO事件
3.) 處理超時(shí)的Handler,如果有超時(shí)的事件,返回(1)。這點(diǎn)我看得最郁悶。
4.) 再分發(fā)處理IO事件
可以看到在處理超時(shí)句柄的時(shí)候,ACE_Dev_Poll_Reactor發(fā)現(xiàn)有超時(shí)的事件會(huì)返回到檢查超時(shí)隊(duì)列。所以如果在Reactor同時(shí)有定時(shí)處理,IO的優(yōu)先級(jí)會(huì)很低。
其實(shí)這個(gè)的設(shè)計(jì)者也知道這個(gè)問(wèn)題。他在代碼中間做了如下的記錄。
int
ACE_Dev_Poll_Reactor::dispatch (Token_Guard &guard)
{
……
// Handle timers early since they may have higher latency
// constraints than I/O handlers. Ideally, the order of
// dispatching should be a strategy...
if ((result = this->dispatch_timer_handler (guard)) != 0)
return result;
由于EPOLL的特性,使用它大部分都是為了處理大規(guī)模的IO請(qǐng)求,定時(shí)器其實(shí)只有少量的需求,不是我們需求的重點(diǎn)。
這個(gè)問(wèn)題到最近的5.6.1版本沒(méi)有得到解決。
我曾經(jīng)反饋過(guò)這個(gè)問(wèn)題。但是得到?jīng)]有明確的解答。解決這個(gè)問(wèn)題的方法其實(shí)也很簡(jiǎn)單,自己重載這個(gè)類(lèi),然后自己實(shí)現(xiàn)相應(yīng)的函數(shù)。觸發(fā)IO事件后立即分發(fā)IO事件,而且加入了一個(gè)IO的優(yōu)先級(jí)別。在多次IO處理的循環(huán)后在進(jìn)入時(shí)間事件處理。保證時(shí)間處理的粒度在1s以?xún)?nèi)基本就可以了。
在程序退出的【注】,我們往往不會(huì)自己關(guān)閉Event_Handler,而寄希望Reactor 的清理。但是實(shí)際情況會(huì)復(fù)雜很多。使用的時(shí)候必須當(dāng)心。
【注】是否要在退出的時(shí)候清理所有分配的內(nèi)存?在普通的操作系統(tǒng)中,程序的退出會(huì)回收所有的分配內(nèi)存。所以很多人會(huì)逃避在最后階段的清理分配的內(nèi)存。但是這實(shí)在不是一個(gè)良好的喜歡。一方面對(duì)于很多OS(比如嵌入系統(tǒng))不會(huì)回收內(nèi)存資源,一些內(nèi)核資源(UNIX)也不會(huì)在進(jìn)程退出后釋放,編程就應(yīng)該要養(yǎng)成清理的好習(xí)慣,更何況不進(jìn)行釋放在內(nèi)存檢查的軟件一般會(huì)報(bào)錯(cuò),如果不清理會(huì)干擾我們對(duì)于內(nèi)存泄露的定位。
10.1 Reactor的close可能不會(huì)關(guān)閉Event_Handler
理論上講,ACE_Reactor提供了一個(gè)close函數(shù),所有的Event_Handler應(yīng)該統(tǒng)一在這個(gè)函數(shù)進(jìn)行關(guān)閉。
ACE_Reactor采用的是模式,封裝了不同Reactor的實(shí)現(xiàn)。這些實(shí)現(xiàn)的close函數(shù)未存在一定的差異性。就我的閱讀和嘗試來(lái)看,Select_Reactor在close函數(shù)關(guān)閉了所有的IO句柄相關(guān)的Event_Handler,而Dev_Poll_Reactor的close實(shí)現(xiàn)就沒(méi)有關(guān)閉。
Select_Reactor的close代碼。
template <class ACE_SELECT_REACTOR_TOKEN> int
ACE_Select_Reactor_T<ACE_SELECT_REACTOR_TOKEN>::close (void)
{
……
//在handler_rep的close函數(shù)會(huì)關(guān)閉所有的register的句柄的handler,調(diào)用他們的
//handle_close函數(shù)
this->handler_rep_.close ();
Dev_Poll_Reactor的close的調(diào)用了函數(shù)ACE_Dev_Poll_Reactor_Handler_Repository::close,而后有逐步調(diào)用了unbind_all,remove_reference。
//close會(huì)經(jīng)過(guò)多級(jí)調(diào)用到ACE_Dev_Poll_Reactor_Handler_Repository:: unbind_all
//unbind被unbind_all函數(shù)調(diào)用decr_refcnt == true
int
ACE_Dev_Poll_Reactor_Handler_Repository::unbind (ACE_HANDLE handle,
bool decr_refcnt)
{……
// remove_reference函數(shù)沒(méi)有調(diào)用handle_close,而是減去了引用計(jì)數(shù)
if (decr_refcnt)
this->handlers_[handle].event_handler->remove_reference ();
……
}
ACE_Event_Handler::Reference_Count
ACE_Event_Handler::remove_reference (void)
{
//如果打開(kāi)了引用計(jì)數(shù),則使用應(yīng)用計(jì)數(shù)方式管理方式。但是代碼默認(rèn)不采用應(yīng)用計(jì)數(shù)模式
//所以下面的代碼都無(wú)法執(zhí)行
if (reference_counting_required)
{
//減去引用計(jì)數(shù)
Reference_Count result =
--this->reference_count_;
//如果已經(jīng)沒(méi)用引用個(gè)數(shù)了,刪除自己。
if (result == 0)
delete this;
}
可以看到ACE_Event_Handler的代碼默認(rèn)不采用應(yīng)用計(jì)數(shù)模式,(eference_counting_required默認(rèn)為DISABLED)而Dev_Poll_Reactor卻非要使用引用計(jì)數(shù)模式去清理Event_Handler。
我對(duì)Dev_Poll_Reactor為什么要設(shè)計(jì)成這樣表示不解。也對(duì)Dev_Poll_Reactor提交過(guò)BUG,但是Dev_Poll_Reactor的開(kāi)發(fā)者不認(rèn)為這樣有什么不妥,本人E文羞澀,無(wú)法說(shuō)服具體的開(kāi)發(fā)人員,不過(guò)在提交BUG時(shí),居然得到了Douglas反饋(他開(kāi)始時(shí)認(rèn)同我的看法),對(duì)于他們的執(zhí)著和認(rèn)真還是表示敬仰。
這個(gè)問(wèn)題是在工作中調(diào)試一個(gè)BUG出現(xiàn)的。
在測(cè)試一個(gè)服務(wù)器的時(shí)候發(fā)現(xiàn)Coredump發(fā)生kill進(jìn)程,讓其退出在之后,會(huì)出現(xiàn)Coredump文件。Coredump顯示出現(xiàn)問(wèn)題的地方在。
#1 0x0805bc7b in ~ACE_Timer_Heap_T (this=0x82d3ec8) at /usr/local/ACE_wrappers/ace/Timer_Queue_T.cpp:442
#2 0x0805b86d in ~ACE_Singleton (this=0x82cca70) at egg_application.cpp:52
#3 0x08056785 in ACE_Singleton<EggSvrdAppliction, ACE_Null_Mutex>::cleanup (this=0x82dfb90)
由于希望改變ACE_Time_Queue的特性(數(shù)量),我替換Reactor的默認(rèn)Time_Queue,所以必須自己銷(xiāo)毀自己管理的TimeQueue。而在外部最后銷(xiāo)毀的時(shí)候出現(xiàn)Coredump。由于和Time_Queue相關(guān),我檢查了所有的Timer相關(guān)的Event_handler,發(fā)現(xiàn)有一個(gè)Event_handler沒(méi)有自己主動(dòng)調(diào)用handler_close釋放,這個(gè)Event_handler只有定時(shí)器,沒(méi)有注冊(cè)任何IO事件。修改代碼為主動(dòng)釋放后,再次測(cè)試就發(fā)現(xiàn)Coredump的問(wèn)題得到解決。
我檢查了一下原有代碼堆棧的調(diào)用順序,找到了問(wèn)題原因。
(1)ACE_Reactor::close,實(shí)際調(diào)用ACE_Select_Reactor::close
(2) Select_Reactor::close 嘗試關(guān)閉所有的IO句柄相關(guān)的Event_handler,但由于Time_Queue是外部傳入的參數(shù),所以不清理Time_Queue。
(3)Time_Queue清理,Time_Queue的析構(gòu)函數(shù)被調(diào)用,Time_Queue的析構(gòu)函數(shù)會(huì)釋放所有的定時(shí)器相關(guān)的Event_handler。而他的釋放還會(huì)調(diào)用hanlder_close。但是這是Reactor對(duì)象已經(jīng)銷(xiāo)毀了。所以造成了Coredump。
注意由于Reactor的封裝了Event_handler定時(shí)器,IO句柄,Notify機(jī)制等回調(diào)接口。所以Event_handler可能只關(guān)聯(lián)到IO句柄,也可能只關(guān)聯(lián)定時(shí)器,同時(shí)Reactor的模型決定了他的內(nèi)部管理是復(fù)雜的。而在釋放的過(guò)程中很可能會(huì)發(fā)生交錯(cuò)的問(wèn)題,而,像上面問(wèn)題的Event_handler就只關(guān)聯(lián)的定時(shí)器,所以在Reactor的close的時(shí)候沒(méi)有關(guān)閉。從而導(dǎo)致在后面的清理工作中產(chǎn)生時(shí)序問(wèn)題。
最簡(jiǎn)單的方式還是自己在程序退出前清理釋放所有的Event_handler.再調(diào)用Reactor的close。
由于我們采用的服務(wù)器一般都是靠紐扣電池作為能源驅(qū)動(dòng)和記錄時(shí)鐘,一般在運(yùn)行一段時(shí)間后都會(huì)出現(xiàn)時(shí)間誤差。所以很多大規(guī)模的分布系統(tǒng)都有校時(shí)操作,特別是一些對(duì)時(shí)鐘要求精確的分布式系統(tǒng)(比如計(jì)費(fèi)等),往往都會(huì)有一個(gè)主機(jī)提供精確時(shí)鐘服務(wù)(其可能采用GPS校時(shí)),其他服務(wù)器通過(guò)這臺(tái)服務(wù)器校時(shí),校時(shí)操作一般都是直接改變系統(tǒng)時(shí)鐘。
ACE的定時(shí)器都是采用Event_Handler進(jìn)行處理,而Event_Handler一般而言都是采用絕對(duì)時(shí)間作為記錄超時(shí)的時(shí)間戳,但是絕對(duì)時(shí)間的方式在系統(tǒng)時(shí)鐘被調(diào)整的時(shí)候,會(huì)導(dǎo)致“丟失”部分定時(shí)器的處理,導(dǎo)致一些問(wèn)題。
在設(shè)置定時(shí)器時(shí),schedule_timer函數(shù)通過(guò)gettimeofday得到定時(shí)器時(shí)間點(diǎn)的時(shí)間。
template <class ACE_SELECT_REACTOR_TOKEN> long
ACE_Select_Reactor_T<ACE_SELECT_REACTOR_TOKEN>::schedule_timer
(ACE_Event_Handler *handler,
const void *arg,
const ACE_Time_Value &delay_time,
const ACE_Time_Value &interval)
{
// schedule_timer記錄的是系統(tǒng)時(shí)間,
if (0 != this->timer_queue_)
return this->timer_queue_->schedule
(handler,
arg,
timer_queue_->gettimeofday () + delay_time,
interval);
}
在派發(fā)定時(shí)器的過(guò)程中也是調(diào)用gettimeofday函數(shù)。
template <class TYPE, class FUNCTOR, class ACE_LOCK> ACE_INLINE int
ACE_Timer_Queue_T<TYPE, FUNCTOR, ACE_LOCK>::expire (void)
{
if (!this->is_empty ())
return this->expire (this->gettimeofday () + timer_skew_);
else
return 0;
}
可以看出,如果在schedule_timer后,將系統(tǒng)時(shí)鐘向前調(diào)節(jié)(調(diào)慢)以后,原有的定時(shí)器將要經(jīng)過(guò)更多的時(shí)間才能觸發(fā)。從而導(dǎo)致這段時(shí)間內(nèi)定時(shí)器無(wú)法觸發(fā)。從而造成定時(shí)器丟失。
這個(gè)問(wèn)題的解決方法有2個(gè),簡(jiǎn)單方法是將系統(tǒng)時(shí)鐘校準(zhǔn)的頻度提高,保證每次校準(zhǔn)的時(shí)候,系統(tǒng)的時(shí)鐘出現(xiàn)的偏差都不會(huì)影響時(shí)鐘的定時(shí)器觸發(fā)。
另外一種是ACE的Timer_Queue自己提供的方法,通過(guò)上面的代碼我們可以發(fā)現(xiàn),其實(shí)ACE_Timer_Queue_T::gettimeofday是一個(gè)調(diào)用的是一個(gè)函數(shù)指針。默認(rèn)使用ACE_OS:: gettimeofday函數(shù),這個(gè)函數(shù)可以替換的。
void gettimeofday (ACE_Time_Value (*gettimeofday)(void));
ACE提供一個(gè)依賴(lài)于操作系統(tǒng)的高解析定時(shí)器,ACE_High_Res_Timer,這個(gè)類(lèi)是通過(guò)OS的TICK數(shù)量來(lái)得到更加精確的時(shí)鐘的【注】。
【注】OS在啟動(dòng)后,都會(huì)有一個(gè)TICK在不斷的計(jì)數(shù),這個(gè)TICK就像一個(gè)打點(diǎn)計(jì)數(shù)器,每次增加1.一般計(jì)數(shù)周期就是一個(gè)CPU周期。
由于CPU的TICK不會(huì)隨著你調(diào)整系統(tǒng)時(shí)鐘而調(diào)整。所以可以看做是一個(gè)相對(duì)值。ACE_High_Res_Timer可以根據(jù)相對(duì)值計(jì)算得到非常精確的程序運(yùn)行時(shí)鐘,。直接使用ACE_High_Res_Timer:: gettimeofday_hr函數(shù)作為ACE_Timer_Queue_T::gettimeofday函數(shù)指針。并且在程序的開(kāi)始部分使用函數(shù),ACE_High_Res_Timer::global_scale_factor (),用于激活高精度定時(shí)器。【注】
【注】這個(gè)方法得益于原來(lái)公司的兩位同事zhangtianhu和liaobincai的一個(gè)終結(jié)。在此懷念一下和他們共事的日子。另外,我沒(méi)有仔細(xì)研究過(guò)這個(gè)方法,由于獲取CPU的TICK的獲取很有可能是一個(gè)內(nèi)核操作,效率可能不高。
采用上述的兩個(gè)方法基本可以避免這個(gè)問(wèn)題。
12 ACE的CDR中的字節(jié)對(duì)齊問(wèn)題
大家應(yīng)該都知道計(jì)算機(jī)中間都有字節(jié)對(duì)齊問(wèn)題。CPU訪問(wèn)內(nèi)存的時(shí)候,如果從特定的地址開(kāi)始訪問(wèn)一般可以加快速度,比如在32位機(jī)器上,如果一個(gè)32位的整數(shù)被放在能被32模除等于0的地址上,只需要訪問(wèn)一次,而如果不在,可能要訪問(wèn)兩次。但是這樣就要求一些數(shù)據(jù)從特定的地址開(kāi)始,而不是順序排放(中間會(huì)有一些空余的地址),這就是字節(jié)對(duì)齊。
而ACE CDR的估計(jì)也是為了加快速度,從而在CDR編碼上默認(rèn)也使用了字節(jié)對(duì)齊。所以在ACE的CDR編解碼過(guò)程中,傳入的參數(shù)地址最好是能符合字節(jié)對(duì)齊規(guī)則,否則可能會(huì)編解碼錯(cuò)誤。
ACE_OutputCDR構(gòu)造函數(shù)會(huì)調(diào)用一個(gè)函數(shù)mb_align調(diào)整傳入的地址參數(shù)成為地址對(duì)齊地址。但是其的調(diào)整函數(shù)ACE_ptr_align_binary不知處于什么考慮,不是按照機(jī)器的對(duì)齊長(zhǎng)度而是采用的 ACE_CDR::MAX_ALIGNMENT(64bit,長(zhǎng)度為8BYTPES)作為參數(shù)地址。那么ACE_OutputCDR的內(nèi)部地址是按照8字節(jié)作為對(duì)齊的,但是ACE_InputCDR卻沒(méi)有將內(nèi)部地址調(diào)整為模除64等于0的地址上,而只是調(diào)整為模除32(在32位機(jī)器上)等于0的地址。
void
ACE_CDR::mb_align (ACE_Message_Block *mb)
{
#if !defined (ACE_CDR_IGNORE_ALIGNMENT)
//如果使用字節(jié)對(duì)齊方式,使用最大的對(duì)齊方式調(diào)整內(nèi)存。調(diào)整為模除64等于0的地址上。
char * const start = ACE_ptr_align_binary (mb->base (),
ACE_CDR::MAX_ALIGNMENT);
#else
……
}
使用一段簡(jiǎn)單的代碼可以測(cè)試發(fā)現(xiàn)這個(gè)問(wèn)題。
char *tmp_buffer = new char [2048];
//使用一個(gè)無(wú)法對(duì)齊的參數(shù)作為ACE_InputCDR,ACE_OutputCDR的參數(shù)地址,
char *tmp_data = tmp_buffer +1;
// output_cdr調(diào)整了對(duì)齊的起始地址為8字節(jié)的默認(rèn)
ACE_OutputCDR output_cdr(tmp_data,512);
ACE_InputCDR input_cdr(tmp_data,512);
ACE_CDR::ULong cdr_long = 123;
bool bret =false;
//
bret = output_cdr.write_ulong(cdr_long);
// cdr_long 不等于123,而是一個(gè)錯(cuò)誤無(wú)效數(shù)據(jù)。
bret = input_cdr.read_ulong(cdr_long);
其實(shí)如果編解碼的BUFF都采用相同的對(duì)齊方式,那么理論上也不應(yīng)該出現(xiàn)問(wèn)題,最多是出現(xiàn)為了對(duì)齊而進(jìn)行填補(bǔ)的空隙,但是這樣能帶來(lái)CPU的效率提升,也是好事。但是由于ACE_OutputCDR的一個(gè)地址調(diào)整。卻可能導(dǎo)致編解碼的BUFFER不一致,我不能肯定這到底是一個(gè)錯(cuò)誤還是作者有他自己的考慮。
這個(gè)問(wèn)題到5.6.1還存在。我已經(jīng)提交了問(wèn)題報(bào)告。
當(dāng)然有一個(gè)方法解決這個(gè)問(wèn)題。就是定義宏ACE_CDR_IGNORE_ALIGNMENT【注】,只要定義了這個(gè)宏,ACE就不會(huì)使用字節(jié)對(duì)齊處理CDR編碼。使用這個(gè)方法的,編碼占用空間會(huì)壓縮一些,但效率上可能低一點(diǎn)(其實(shí)未必,因?yàn)闉榱俗止?jié)對(duì)齊還要耗費(fèi)一些計(jì)算時(shí)間),
【注】ACE不知道為什么在代碼中使用兩個(gè)不使用字節(jié)對(duì)齊的宏,一個(gè)是在CDR_Base.h CDR_Base.cpp 文件中使用的是ACE_CDR_IGNORE_ALIGNMENT,在CDR_Stream.cpp和CDR_Stream.h文件上使用的宏ACE_LACKS_CDR_ALIGNMENT。
我一般將兩個(gè)宏都定義上。
這個(gè)純屬個(gè)人感覺(jué)(偏見(jiàn))。我有如下理由不使用ACE的容器:
l 一些實(shí)現(xiàn)不符合大家對(duì)于容器的認(rèn)識(shí),比如ACE_DLList,在其中存放的居然是對(duì)象的指針而不是拷貝。你還必須記住去釋放ACE_DLList內(nèi)部管理的指針。
l ACE容器的迭代器不符合STL的要求,從而造成ACE的容器無(wú)法使用STL的各種模板算法和函數(shù)。總不能因?yàn)?/span>ACE容器失去STL算法這片森林吧。
l 現(xiàn)在的編譯器上已經(jīng)非常普遍實(shí)現(xiàn)了STL,想找一個(gè)還不支持STL的編譯器應(yīng)該都不容易了。
l ACE的容器中間有大量指針,所以ACE的容器也不可能用在共享內(nèi)存中。其的應(yīng)用場(chǎng)景和STL沒(méi)有本質(zhì)區(qū)別。
ACE的文檔《The.ACE.Programmers.Guide》中間也說(shuō)過(guò):
That being said, the standard C++ containers are recommended for application development when you are using ACE.
所以在可以使用STL的情況下,還是優(yōu)先使用STL。
ACE的日志部分是一個(gè)非常漂亮的實(shí)現(xiàn),在多線程和多進(jìn)程模型下都能較好的效率和安全使用。但是卻又少量的不足,讓人意猶未盡。
ACE日志對(duì)于時(shí)間戳的格式是固定的,采用的是格式,這個(gè)格式在西方人看起來(lái)估計(jì)還比較順眼,在東方人眼中卻不如人意。更好的方式當(dāng)然是時(shí)間戳的函數(shù)可以重載。或者用函數(shù)對(duì)象(指針)作為參數(shù)傳入。
雖然這部分代碼可以重載解決這個(gè)問(wèn)題,但是要大動(dòng)干戈只修正這個(gè)問(wèn)題感覺(jué)卻又不值得的。
ACE提供了一個(gè)日志策略類(lèi)ACE_Logging_Strategy輔助大家定義日志策略。但是他的初始化參數(shù)卻是命令行參數(shù),而不是變量參數(shù)。
int
ACE_Logging_Strategy::init (int argc, ACE_TCHAR *argv[])
你必須使用這樣的命令行去初始化日志策略模塊。
-m1024 -N10 -fSTDERR|OSTREAM -s../log/c4ad.log
試問(wèn)有幾個(gè)服務(wù)器的開(kāi)發(fā)人員會(huì)將這些日志策略的初始化放到命令行參數(shù)上去。
ACE_Logging_Strategy的日志文件的分割策略采用的是按照文件大小分割文件,文件的序號(hào)采用滾動(dòng)的,但這種日志分割方式無(wú)法根據(jù)文件時(shí)間了解日志內(nèi)容,(由于文件序號(hào)要滾動(dòng),序號(hào)文件的最后修改時(shí)間都一樣),你只能grep所有的日志尋找你要的內(nèi)容。
而在我看來(lái),最好日志分割方式肯定是按照日期進(jìn)行分割日志文件。每天創(chuàng)建一個(gè)新的日志文件,可以方便分割日志。清理和管理的工作量大大降低。
ACE_Logging_Strategy采用的是日志槽的方式Enable或者Disable某些級(jí)別的日志。但是感覺(jué)多少有點(diǎn)不自然的,ACE自己的日志級(jí)別本身就是分級(jí)的。個(gè)人感覺(jué)應(yīng)該是如果日志輸出的日志級(jí)別大于定義的級(jí)別就能輸出應(yīng)該是一個(gè)更好的選擇。
解決ACE_Logging_Strategy的問(wèn)題最好的辦法還是擴(kuò)展這個(gè)類(lèi)。實(shí)現(xiàn)自己的日志策略類(lèi)。
ACE_Time_Value是使用ACE會(huì)大量使用類(lèi)。但是他的部分函數(shù)沒(méi)有高效的實(shí)現(xiàn)。比如構(gòu)造函數(shù):
ACE_INLINE
ACE_Time_Value::ACE_Time_Value (time_t sec, suseconds_t usec)
和set函數(shù)
ACE_INLINE void
ACE_Time_Value::set (time_t sec, suseconds_t usec)
為了規(guī)范用戶(hù)的賦值,在這些函數(shù)的最后都會(huì)調(diào)用normalize函數(shù)。
void ACE_Time_Value::normalize (void)
但如果你的賦值的微秒數(shù)值不合適(過(guò)大)時(shí),normalize卻不是一個(gè)高效實(shí)現(xiàn)。下面簡(jiǎn)單摘取normalize的一段代碼。
void
ACE_Time_Value::normalize (void)
{
//如果賦值的大于微秒數(shù)值大于1s。
if (this->tv_.tv_usec >= ACE_ONE_SECOND_IN_USECS)
{
/*! \todo This loop needs some optimization. */
//作者都認(rèn)為這個(gè)代碼要優(yōu)化
//那么進(jìn)入循環(huán),每次減去1000000的微秒單位,在秒的單位+1,上帝呀。
do
{
++this->tv_.tv_sec;
this->tv_.tv_usec -= ACE_ONE_SECOND_IN_USECS;
}
while (this->tv_.tv_usec >= ACE_ONE_SECOND_IN_USECS);
}
…………
}
很不理解為什么會(huì)寫(xiě)成如此的低效。為什么不直接使用除法呢,我很不理解。所以如果你在代碼的主循環(huán)中如果使用了ACE_Time_Value,使用上面的那些函數(shù)就可能掉入陷阱。
解決方法是盡量使用函數(shù)sec和usec賦值,這些函數(shù)不會(huì)調(diào)用normalize,這兩個(gè)函數(shù)會(huì)直接賦值。如果非要使用上面的那些函數(shù)方式,也一定不要使用過(guò)大的(錯(cuò)誤的)時(shí)間參數(shù)。
這個(gè)問(wèn)題到5.6.1還沒(méi)有得到修正。
ACE的非阻塞網(wǎng)絡(luò)函數(shù)參數(shù)設(shè)計(jì)有不合理的地方。ACE_SOCK_Stream和ACE_SOCK_Connector在非阻塞的的調(diào)用的接口對(duì)于ACE_Time_Value *timeout參數(shù)的使用不一致,一個(gè)要使用NULL,一個(gè)卻要使用ACE_Time_Value::zero。
ACE_SOCK_Stream,非阻塞調(diào)用send函數(shù)的時(shí)候【注】,timeout參數(shù)必須填寫(xiě)為NULL。它最后調(diào)用的是ACE::send。將ACE_Time_Value填寫(xiě)為ACE_Time_Value::zero (0,0)是不行的。如果填寫(xiě)ACE_Time_Value::zero,會(huì)大大降低這個(gè)非阻塞調(diào)用的性能。
ssize_t
ACE::send (ACE_HANDLE handle,
const void *buf,
size_t n,
int flags,
const ACE_Time_Value *timeout)
{
if (timeout == 0)
return ACE_OS::send (handle, (const char *) buf, n, flags);
else
{
…………
}
}
timeout);
注意使用非阻塞的的IO要調(diào)用recv,send函數(shù),而不要調(diào)用recv_n,send_n這些函數(shù)接口,這些函數(shù)接口如果timeout參數(shù)傳遞NULL,表示阻塞。
另外非阻塞IO還是要自己設(shè)置Socket的選項(xiàng)。
但是ACE_SOCK_Connector卻采用另外一個(gè)封裝方式,其是傳入一個(gè)NULL表示阻塞,而傳入ACE_Time_Value::zero (0,0)表示進(jìn)行非阻塞鏈接操作。
* @param timeout Pointer to an @c ACE_Time_Value object with amount
* of time to wait to connect. If the pointer is 0
* then the call blocks until the connection attempt
* is complete, whether it succeeds or fails. If
* *timeout == {0, 0} then the connection is done
* using nonblocking mode. In this case, if the
* connection can't be made immediately, this method
* returns -1 and errno == EWOULDBLOCK.
int connect (ACE_SOCK_Stream &new_stream,
const ACE_Addr &remote_sap,
const ACE_Time_Value *timeout = 0,
const ACE_Addr &local_sap = ACE_Addr::sap_any,
int reuse_addr = 0,
int flags = 0,
int perms = 0,
int protocol = 0);
大家在處理這些IO時(shí)務(wù)必當(dāng)心。
這個(gè)”陷阱”的說(shuō)法有點(diǎn)吹毛求疵,ACE提供了一種很前衛(wèi)的Makefile方式,他定義了Makefile的基礎(chǔ)變量,以及包括規(guī)則。如果使用他來(lái)輔助Makefile的書(shū)寫(xiě),特別是在跨平臺(tái)開(kāi)發(fā)中,你可以大大節(jié)省Makefile開(kāi)發(fā)時(shí)間。
BIN = hello_ace
BUILD = $(VBIN)
SRC = $(addsuffix .cpp,$(BIN))
LIBS = -lMyOtherLib
LDFLAGS = -L$(PROJ_ROOT)/lib
#---------------------------------------------------
#Include macros and targets
#---------------------------------------------------
include $(ACE_ROOT)/include/makeinclude/wrapper_macros.GNU
include $(ACE_ROOT)/include/makeinclude/macros.GNU
include $(ACE_ROOT)/include/makeinclude/rules.common.GNU
include $(ACE_ROOT)/include/makeinclude/rules.nonested.GNU
include $(ACE_ROOT)/include/makeinclude/rules.bin.GNU
include $(ACE_ROOT)/include/makeinclude/rules.local.GNU
但是麻煩就在于ACE的這些Makefile方法幾乎沒(méi)有一個(gè)文檔幫助說(shuō)明,我一直無(wú)法理解$VBIN到底是什么。這也許,另外,定義到規(guī)則這一層也大大限制了大家對(duì)Makefile的擴(kuò)展能力。這就有一點(diǎn)點(diǎn)高不成低不就的味道了,Makefile的新手幾乎不可能了解ACE的Makefile,老手又會(huì)因?yàn)樘厥獾男枨蟮貌坏綕M(mǎn)足而躊躇。而我個(gè)人一般只使用ACE定義的Makefile變量。這些變量大部分在wrapper_macros.GNU,platform_macros.GNU
表2 ACE Mafile的變量定義
變量
|
描述
|
AR
|
ar 命令的名字
|
ARFLAGS
|
ar 的參數(shù)
|
CC
|
C編譯器的命令的
|
CXX
|
C++編譯器的命令
|
RC
|
資源編譯器命令的名字
|
COMPILE.c
|
編譯C文件的命令行, 一般為:$(CC) $(CFLAGS) $(CPPFLAGS) -c
|
COMPILE.cc
|
編譯C++文件的命令行,一般為:$(CXX) $(CCFLAGS) $(CPPFLAGS) $(PTDIRS) –c
|
COMPILEESO.cc
|
$(CXX) $(CCFLAGS) $(CPPFLAGS) $(PTDIRS),沒(méi)太搞明白,不知道為什么和SO有關(guān),好像是為了修正錯(cuò)誤增加的。不理也罷
|
CPPFLAGS
|
C,C++語(yǔ)言編譯的預(yù)標(biāo)志,比如DEFINDE等. CPPFLAGS += $(DEFFLAGS) $(INCLDIRS)
|
CFLAGS
|
C語(yǔ)言編譯選項(xiàng)
|
CCFLAGS
|
C++語(yǔ)言編譯選項(xiàng)
|
DCFLAGS
|
Debugging 程序的C語(yǔ)言編譯選項(xiàng),一般在有debug=1變量時(shí)有效
|
DCCFLAGS
|
Debugging 程序的C++語(yǔ)言編譯選項(xiàng),一般在有debug=1變量時(shí)有效
|
DEFFLAGS
|
C++ 預(yù)處理的DEFINE部分
|
DLD
|
dynamic linker 動(dòng)態(tài)庫(kù)link命令的名字,
|
LD
|
linker 命令的名字
|
IDL
|
CORBA IDL compiler 命令的名字
|
INCLDIRS
|
INCLUDE的頭文件
|
LDFLAGS
|
ld linker flags
|
LINK.c
|
鏈接C文件的命令行
|
LINK.cc
|
鏈接C++文件的命令行,一般為:$(PURELINK) $(PRELINK) $(LD) $(CCFLAGS) $(CPPFLAGS) $(PTDIRS)
|
MAKEFLAGS
|
Flags that are passed into the compilation from the commandline
|
OCFLAGS
|
Optimizing 程序的C語(yǔ)言編譯選項(xiàng)
|
OCCFLAGS
|
Optimizing 程序的C++語(yǔ)言編譯選項(xiàng)
|
PIC
|
PIC就是position independent code
|
PCFLAGS
|
profiling 程序的C語(yǔ)言編譯選項(xiàng) profiling是什么不要問(wèn)我。
|
PCCFLAGS
|
profiling 程序的C++語(yǔ)言編譯選項(xiàng)
|
PRELINK
|
LINK之前執(zhí)行的命令
|
PURELINK
|
purify 執(zhí)行的命令,purify是什么不要問(wèn)我。
|
PWD
|
得到當(dāng)前目錄的命令
|
PTDIRS
|
模板文件的路徑定義
|
RM
|
刪除工具的命令
|
ACE_MKDIR
|
遞歸創(chuàng)建的目錄
|
SOFLAGS
|
生成.so庫(kù)時(shí)候的參數(shù)
|
SOLINK.cc
|
生成.so庫(kù)時(shí)候的命令行
|
VAR
|
Variant identifier suffix
|
VDIR
|
Directory for object code .obj/
|
VSHDIR
|
Directory for shared object code .shobj/
|
看起來(lái)變量很多,其實(shí)要記住和使用的可以很少,你需要留意的主要是.cc結(jié)尾的變量就可以了。我們可以使用ACE MakreFile的變量,方便我們的Makefile開(kāi)發(fā)。比如:
我的Makefile,就使用了$(LINK.cc), $(COMPILE.cc)兩個(gè)宏。
#使用ACE的wrapper_macros.GNU的定義變量
include $(ACE_ROOT)/include/makeinclude/wrapper_macros.GNU
#得到C,CPP文件的列表
SRC_FILE = $(wildcard ./*.cpp )
#通過(guò).C,.CPP文件名稱(chēng)得到.O文件名稱(chēng),.o 文件放在../../object/exampleexe/目錄下
O_FILE = $(patsubst ./%.cpp, ../../object/exampleexe/%.o, $(CPP_FILE))
#輸出文件exe_file
OUTFILE = ../../bin/exampleexe
# LIB_ALL為 –l文件和-L目錄的定義
$(OUTFILE): $(O_FILE)
$(LINK.cc) -o$(OUTFILE) $(O_FILE) $(LIB_ALL)
#.o輸出文件放在../../object/目錄下
../../object/exampleexe/%.o : ./%.cpp
$(COMPILE.cc) $(INC_ALL) $< -o $@
clean:
$RM -f $(OUTFILE) $(O_FILE)
是不是也很酷,輕松實(shí)現(xiàn)Makefile的跨越平臺(tái)移植。
在文檔《ACE Programmer's Guide, The: Practical Design Patterns for Network and Systems Programming》中介紹了一種與位置無(wú)關(guān)的共享內(nèi)存分配,但是實(shí)際上這種方式并不是太理想。按照文章中的介紹的方式,其實(shí)主要是采用ALWAYS_FIXED參數(shù),使用制定的基地址作為共享內(nèi)存的地址。同時(shí)使用輔助類(lèi)保證2個(gè)進(jìn)程使用相對(duì)地址使用共享內(nèi)存。
ACE_MMAP_Memory_Pool_Options options
(ACE_DEFAULT_BASE_ADDR,
ACE_MMAP_Memory_Pool_Options::ALWAYS_FIXED);
ACE_NEW_RETURN (g_allocator,
ALLOCATOR (BACKING_STORE,
BACKING_STORE,
&options),
-1);
ACE_DEBUG ((LM_DEBUG,
ACE_TEXT ("Mapped to base address %@\n"),
g_allocator->base_addr ()));
showRecords ();
但是,首先要求大家能使用相同的基地址,按照ACE給出的例子。其給出默認(rèn)基地址一個(gè)宏ACE_DEFAULT_BASE_ADDR(在Linux下是0x80000000)。因?yàn)榈刂房臻g管理都是操作系統(tǒng)的負(fù)責(zé)的事情,所以要求使用同一塊共享內(nèi)存的2個(gè)進(jìn)程分配的基地址是一樣的是很不靠譜的事情。采用這種方式可能有2個(gè)后果,第一如果你要使用多個(gè)共享內(nèi)存,你要自己計(jì)算管理進(jìn)程空間,第二你程序可移植性很低,甚至?xí)霈F(xiàn)在一臺(tái)機(jī)器上可以運(yùn)行,在另外1臺(tái)機(jī)器無(wú)法運(yùn)行。所以大家慎用這個(gè)特性比較好。把程序的可靠運(yùn)行寄托于運(yùn)氣好,這不應(yīng)該是一個(gè)程序員的作風(fēng)。
所以對(duì)于共享內(nèi)存,如果希望實(shí)現(xiàn)與位置無(wú)關(guān)的分配,我個(gè)人的忠告如下:
l 一開(kāi)始分配足夠的空間,不要再進(jìn)行擴(kuò)展【注】。因?yàn)閿U(kuò)展共享內(nèi)存可能意味著原來(lái)所有的共享內(nèi)存相關(guān)指針會(huì)失效。
l 各自進(jìn)程管理自己的地址空間,共享內(nèi)存內(nèi)部不要保存任何指針(特別不要在共享內(nèi)存內(nèi)保存指針),所有的地址都使用相對(duì)值。這樣才能保證重入,和基礎(chǔ)地址變化下不出現(xiàn)問(wèn)題。
《ACE Programmer's Guide, The: Practical Design Patterns for Network and Systems Programming》中間還提出過(guò)處理共享內(nèi)存池封裝,但考慮到涉及所有的共享內(nèi)存地址的都要調(diào)整。不是太認(rèn)可這種方式。
另外由于ACE的容器都使用了指針,不建議在共享內(nèi)存中使用ACE的容器。
如果你的應(yīng)用有大量的定時(shí)器,你最好自己控制Timer_Queue的尺寸。原因如下。默認(rèn)的ACE的Timer_Queue初始化的尺寸不大,一般只有44個(gè)。而原有的尺寸不能滿(mǎn)足你的要求的時(shí)候,Timer_Queue會(huì)自動(dòng)增長(zhǎng),以Timer_Heap為例,增長(zhǎng)的方式是擴(kuò)大一倍空間。在性能要求嚴(yán)格環(huán)境下,多次增長(zhǎng)隊(duì)列的尺寸對(duì)性能會(huì)造成一定的沖擊。下面是空間調(diào)整函數(shù)grow_heap的部分代碼剖析。
template <class TYPE, class FUNCTOR, class ACE_LOCK> void
ACE_Timer_Heap_T<TYPE, FUNCTOR, ACE_LOCK>::grow_heap (void)
{
//調(diào)整為最大尺寸的兩倍
size_t new_size = this->max_size_ * 2;
ACE_Timer_Node_T<TYPE> **new_heap = 0;
//NEW新的空間,將原有的空間的數(shù)據(jù)拷貝回來(lái)。
ACE_NEW (new_heap,
ACE_Timer_Node_T<TYPE> *[new_size]);
ACE_OS::memcpy (new_heap,
this->heap_,
this->max_size_ * sizeof *new_heap);
delete [] this->heap_;
this->heap_ = new_heap;
//后面還有多個(gè)空間要擴(kuò)展和調(diào)整
……
this->max_size_ = new_size;
}
其實(shí)這和std::vector一樣,如果你知道要使用多少空間,先調(diào)用reserve預(yù)分配空間會(huì)大大加快后面的執(zhí)行速度。如果你知道要使用多少個(gè)定時(shí)器,告知底層,它會(huì)幫你提前分配好空間,否則他會(huì)采用他認(rèn)為合理的方式和尺寸。
所以最好的方法是你先估算你大致需要使用的Timer數(shù)量,在初始化是告訴Timer_Queue。但是Reactor沒(méi)有辦法通過(guò)使用參數(shù)調(diào)整Time_Queue的大小,你必須自己進(jìn)行替換Time_Queue來(lái)實(shí)現(xiàn)目的。方法大致如下:
ACE_Timer_Queue *timer_queue_=NULL;
//根據(jù)自己的需要調(diào)整Time_Queue的尺寸
timer_queue_ = new ACE_Timer_Heap(maxaccept + maxconnect + 16);
ACE_Reactor::instance(new ACE_Reactor(new ACE_Select_Reactor(NULL,timer_queue_,1),1),1);
這樣你就替換了Reactor的Timer_Queue,同時(shí)你要記住在程序運(yùn)行退出前自己釋放的你申請(qǐng)的timer_queue_;
這一節(jié)列一些ACE使用中要注意的一些問(wèn)題。
由于為了一些自己需要的特性,我一般會(huì)自己初始化ACE_Reactor,而不是讓系統(tǒng)默認(rèn)初始化。要注意必須在程序的最開(kāi)始就初始化ACE_Reactor。
由于ACE的很多代碼都會(huì)使用ACE_Reactor,包括日志的策略類(lèi)。所以ACE_Reactor必須在這些代碼前面,否則會(huì)出現(xiàn)奇怪的錯(cuò)誤,比如無(wú)法響應(yīng)某些IO,我至少掉到這個(gè)陷阱里面5次。
有OO基礎(chǔ)的程序都會(huì)放資源的釋放放入析構(gòu)中間去。所以我看到ACE_SOCK_Stream也以為他的在析構(gòu)中關(guān)閉Socket的句柄,但是事實(shí)是ACE_SOCK_Stream必須自己顯式調(diào)用close函數(shù)關(guān)閉Socket句柄。
當(dāng)然,這倒不是ACE的設(shè)計(jì)缺陷,而是ACE的ACE_SOCK_Stream是一個(gè)可以出現(xiàn)在堆棧,可以作為參數(shù)傳遞,進(jìn)行賦值的類(lèi),如果在析構(gòu)中關(guān)閉,就無(wú)法實(shí)現(xiàn)這些功能了。
實(shí)現(xiàn)決定設(shè)計(jì)。辨證呀。
Reactor的handle_events參數(shù)里面的有一個(gè)ACE_Time_Value參數(shù),注意這個(gè)參數(shù)是一個(gè)傳入傳出參數(shù)。
virtual int handle_events (ACE_Time_Value &max_wait_time);
由于Reactor內(nèi)部同時(shí)要管理定時(shí)器和IO句柄,所以ACE很可能不能等待你制定的時(shí)間長(zhǎng)度,所以他會(huì)在傳出參數(shù)告訴你剩余的等待時(shí)間。這時(shí)你可以讓ACE繼續(xù)等待剩余時(shí)間。但在主循環(huán)處理中,你不能這樣做,因?yàn)榻?jīng)過(guò)多次調(diào)用后,ACE_Time_Value參數(shù)會(huì)變成0(ACE_Time_Value::zero)。這是會(huì)導(dǎo)致hanlde_events空轉(zhuǎn),會(huì)導(dǎo)致CPU占用率很高。
對(duì)于大部分主循環(huán)的程序,都不需要這樣做,而應(yīng)該重新制定一個(gè)等待時(shí)間。
ACE_Singleton的模板參數(shù)是可以帶一個(gè)鎖參數(shù)的。
template <class TYPE, class ACE_LOCK>
class ACE_Singleton : public ACE_Cleanup
但你可能會(huì)錯(cuò)誤理解這個(gè)鎖參數(shù)的用途。
typedef ACE_Singleton<Manager, ACE_Thread_Mutex> MANAGER;
MANAGER::instance()->ProcessFunA();
初學(xué)者可能會(huì)疑惑加鎖的是不是ProcessFunA,的處理被加鎖了。但是實(shí)際上ACE_Singleton的鎖只保護(hù)ACE_Singleton內(nèi)部的指針?lè)峙浜弯N(xiāo)毀不出現(xiàn)重入。也就是保護(hù)instance函數(shù)內(nèi)部的指針?lè)峙浜歪尫挪糠帧4a剖析如下:
template <class TYPE, class ACE_LOCK> TYPE *
ACE_Singleton<TYPE, ACE_LOCK>::instance (void)
{
//加鎖部分的代碼,使用GUARD方式保護(hù)new
ACE_GUARD_RETURN (ACE_LOCK, ace_mon, *lock, 0);
if (singleton == 0)
{
ACE_NEW_RETURN (singleton, (ACE_Singleton<TYPE, ACE_LOCK>), 0);
}
……
return &singleton->instance_;
}
其實(shí)理解函數(shù)棧調(diào)用的兄弟應(yīng)該很容易理解這個(gè)問(wèn)題,ProcessFunA 函數(shù)入棧的時(shí)候instance函數(shù)已經(jīng)出棧了。instance函數(shù)內(nèi)部加(解)的鎖無(wú)法影響后續(xù)的調(diào)用。
這兒只是分析(猜測(cè))一下ACE_DEBUG兩層括號(hào)的來(lái)由。用習(xí)慣了Windows下面跟蹤宏TRACE的人開(kāi)始用ACE的調(diào)試宏ACE_DEBUG的宏都會(huì)有點(diǎn)不習(xí)慣,因?yàn)槟惚仨殞?xiě)兩層括號(hào)。
#if defined (ACE_NLOGGING)
#define ACE_DEBUG(X) do {} while (0) /*注意ACE定義的是(X)*/
#else
#define ACE_DEBUG(X) \
do { \
ACE_Log_Msg *ace___ = ACE_Log_Msg::instance (); \
ace___->log X; \ /*注意這兒,這個(gè)奇怪的寫(xiě)法*/
} while (0)
#endif
//使用實(shí)例,
ACE_DEBUG((LM_ERROR,"i=%d.\n",i++));
比較起來(lái),對(duì)于Windows下的TRACE宏的定義如下:
#ifdef _DEBUG
#define TRACE ATLTRACE
#else
#define TRACE __noop /* MSVC特有的一個(gè)標(biāo)識(shí)符,用于忽視后面的參數(shù) */
#endif
而ACE_DEBUG的定義比TRACE的定義是多一層(X)的,所以你必須寫(xiě)兩層括號(hào),ACE實(shí)際上將內(nèi)層括號(hào)的內(nèi)容全部作為宏參數(shù)使用了。
我曾經(jīng)對(duì)這兩層括號(hào)疑惑了很久。因?yàn)槲矣X(jué)得可以采用其他方法繞開(kāi)兩個(gè)括號(hào),(你可以寫(xiě)一個(gè)日志類(lèi)嘗試一下)
#if defined (ACE_NLOGGING)
// 直接定義為一個(gè)函數(shù)的名字,當(dāng)然這兒還要改寫(xiě)其他的很多代碼
#define Z_DEBUG ACE_Log_Msg::instance ()->log
#else
#define Z_DEBUG
#endif
這樣的在沒(méi)有定義ACE_NLOGGING的時(shí)候,Z_DEBUG(LM_ERROR,"i=%d.\n",i++);會(huì)被替換成,(LM_ERROR,"i=%d.\n",i++),這樣也不會(huì)有任何輸出效果。
直到有一次發(fā)現(xiàn)GCC2.9的環(huán)境下編譯類(lèi)似代碼,GCC會(huì)對(duì)這樣的代碼會(huì)產(chǎn)生告警,我大致明白了ACE_DEBUG設(shè)計(jì)者的苦衷。只有雙層括號(hào)的方法才能徹底讓這行代碼不起任何告警。
另外使用兩層括號(hào)也有性能上的好處,大家注意代碼被替換成(LM_ERROR,"i=%d.\n",i++)后,i++的代碼還是要執(zhí)行,在我自己測(cè)試中,即使是在GCC的O3級(jí)別的優(yōu)化編譯中,這樣的代碼也不會(huì)被優(yōu)化掉。而如果采用ACE_DEBUG的設(shè)計(jì),統(tǒng)一替換為do {} while (0),這行代碼則必然將被優(yōu)化掉。而對(duì)于MSVC的編譯器,他提供一個(gè)特別的標(biāo)識(shí)符__noop幫助編譯器優(yōu)化。
大學(xué)畢業(yè)生中能成為好的程序員絕對(duì)不是純粹考試得高分死記公式拿獎(jiǎng)學(xué)金的同學(xué) ,而是那些熬夜寫(xiě)代碼的狂人,哈哈。
計(jì)算機(jī)是一門(mén)實(shí)踐科學(xué),你只有不斷嘗試才能進(jìn)步。
好像是Linus(雖然他好像有點(diǎn)抵觸C++,哈哈),好像是Linus Torvalds在回答一個(gè)提問(wèn)者時(shí)說(shuō):“請(qǐng)去閱讀我的代碼”。了解一個(gè)實(shí)現(xiàn),發(fā)現(xiàn)問(wèn)題的最好方式還是閱讀源代碼。代碼面前,了無(wú)秘密。
當(dāng)然ACE的代碼閱讀起來(lái)不是一件那么舒心的事情。開(kāi)發(fā)者們采用的是一些非常傳統(tǒng)的UNIX習(xí)慣,比如對(duì)齊方式采用2個(gè)空格縮進(jìn),單行if語(yǔ)句不用{}包含,稍顯奇特的inc文件方式,另外,為了支持跨平臺(tái)特性,ACE的代碼用了大量的宏。這都無(wú)疑增加了閱讀的難度。不過(guò)總體說(shuō)了,ACE的代碼比較起Linux內(nèi)核代碼和很多其他類(lèi)庫(kù)的代碼還是好的多,至少注釋很清晰,而且Doxgen生產(chǎn)的文檔很酷,也夠用。
由于ACE是一個(gè)跨平臺(tái)實(shí)現(xiàn)。如果你了解平臺(tái)的實(shí)現(xiàn)。不光你閱讀代碼的速度會(huì)快很多,也會(huì)讓你對(duì)實(shí)現(xiàn)的困惑就會(huì)越少,讓你的代碼避開(kāi)效率的陷阱,你的實(shí)現(xiàn)就會(huì)越高效。
不需要OO的封裝,不用美妙的設(shè)計(jì)模式,沒(méi)有對(duì)效率的執(zhí)著追求,沒(méi)有驚艷的范化設(shè)計(jì),用C++干什么?但沒(méi)有這些信仰,也就不會(huì)有ACE,而且沒(méi)有這些信仰要程序員做什么?
在ACE的使用過(guò)程中,發(fā)現(xiàn)ACE的主要問(wèn)題出在一些高階實(shí)現(xiàn)上。所以如果你要使用高階特性最好能了解背后的實(shí)現(xiàn)。
多用ACE,將發(fā)現(xiàn)的問(wèn)題反饋給ACE的開(kāi)發(fā)者和ACE社區(qū)。
筆名:雁渡寒潭(insailer@gmail.com)
曾星 騰訊公司互動(dòng)娛樂(lè)后臺(tái)開(kāi)發(fā)程序員,目前從事游戲后臺(tái)設(shè)計(jì)開(kāi)發(fā)
個(gè)人興趣范圍:大規(guī)模分布系統(tǒng)的架構(gòu)設(shè)計(jì),高容量,大壓力的服務(wù)器設(shè)計(jì);跨平臺(tái)開(kāi)發(fā);數(shù)據(jù)庫(kù)的設(shè)計(jì),原理和調(diào)優(yōu);多核(CPU)環(huán)境下的程序設(shè)計(jì);OO和設(shè)計(jì)模式;C++和STL以及模板,ACE。歡迎大家交流。
表3 參考的文檔
參考書(shū)目
|
作者/譯者
|
說(shuō)明
|
《C++ Network Programming Volume 1_Mastering Complexity With ACE and Patterns》
|
Douglas C. Schmidt, Stephen D. Huston
|
很多問(wèn)題在這本書(shū)的副欄都有描述,如果你看的很認(rèn)真,也許不會(huì)想我這樣碰暗礁。
|
《C++網(wǎng)絡(luò)編程卷1:運(yùn)用ACE和模式消除復(fù)雜性》
|
於春景
|
|
《C++ Network Programming Volume 2 - Systematic Reuse with ACE and Frameworks》
|
Douglas C. Schmidt, Stephen D. Huston
|
很多問(wèn)題在這本書(shū)的副欄都有描述,如果你看的很認(rèn)真,也許不會(huì)想我這樣碰暗礁。
|
《C++網(wǎng)絡(luò)編程,卷2,基于ACE和框架的系統(tǒng)化復(fù)用》
|
馬維達(dá)
|
|
《The.ACE.Programmers.Guide》
|
Stephen D. Huston, James CE Johnson, Umar Syyid
|
|
《ACE程序員指南》
|
馬維達(dá)
|
|
《ACE自適配通信環(huán)境中文技術(shù)文檔》
|
馬維達(dá)
|
|
ACE html
|
ACE用Doxgen自動(dòng)生成的文檔
|
|
此文檔是耗費(fèi)兩年時(shí)間總結(jié)一些自己在使用ACE的7年中發(fā)現(xiàn)的一些問(wèn)題,在湊夠了20個(gè)標(biāo)題后才進(jìn)行發(fā)布。后面也許會(huì)根據(jù)自己的一些新的發(fā)現(xiàn)修正補(bǔ)充一下文檔,也許。
本著自由的精神,閱讀者可以無(wú)須授權(quán)就可以自由的轉(zhuǎn)載這個(gè)文檔,我只保留作者的署名權(quán)利,也就是說(shuō),你轉(zhuǎn)載只需保留這段說(shuō)明和文檔的完整性(但你不能修改這個(gè)文檔,謝謝)。
這篇文檔也是為了回饋一下這些年來(lái)為自由軟件奮斗的人,也謝謝周?chē)阄乙黄鹜?/span>ACE 的Rong,Sonicmao,Awayfang等兄弟們。最后感謝一下Annie,她忍受了我整理文檔而不陪她看電視。