• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            sherrylso

            C++博客 首頁 新隨筆 聯系 聚合 管理
              18 Posts :: 0 Stories :: 124 Comments :: 0 Trackbacks

            2011年2月18日 #

            Java通過JNI機制調用c/c++寫的native程序。c/c++開發的native程序需要遵循一定的JNI規范,下面的例子就是一個JNI函數聲明:
            JNIEXPORT jint JNICALL Java_jnitest_MyTest_test
              (JNIEnv 
            * env, jobject obj, jint arg0);
            JVM負責從Java Stack轉入C/C++ Native Stack。當Java進入JNI調用,除了函數本身的參數(arg0),會多出兩個參數:JNIEnv指針和jobject指針。
            JNIEnv指針是JVM創建的,用于Native的c/c++方法操縱Java執行棧中的數據,比如Java Class, Java Method等。

            首先,JNI對于JNIEnv的使用, 提供了兩種語法: c語法以及c++語法,如下:
            c語法:
            jsize len = (*env)->GetArrayLength(env,array);
            c++語法:
            jsize len =env->GetArrayLength(array);
            (注:由于C語言并不支持對象的概念,所以C語法中需要把env作為第一個參數傳入,類似于C++的隱式參數this指針).


            另外: JNIEnv有幾個設計的原則:
            第一、JNIEnv指針被設計成了Thread Local Storage(TLS)變量,也就是說每一個Thread, JNIEnv變量都有獨立的Copy。這樣做的原因主要是考慮到:
            由于JVM要運行在多個平臺(除了主流的Windows,Linux等平臺),JNI內部實現很多要依賴到TLS, 為了減少對TLS的依賴,所有TLS based的數據都會存放于JNIEnv中。這樣相當于只依賴一個TLS based的變量JNIEnv。由于JNIEnv指針是TLS的,所以你不能把Thead#1使用的JNIEnv傳給Thread#2使用。

            第二、JNIEnv中定義了一組函數指針,c/c++ Native程序是通過這些函數指針操縱Java數據。這樣設計的好處是:你的c/c++ 程序不需要依賴任何函數庫,或者DLL。由于JVM可能由不同的廠商實現,不同廠商有自己不同的JNI實現,如果要求這些廠商暴露約定好的一些頭文件和庫,這不是靈活的設計。
            而且使用函數指針表的另外一個好處是: JVM可以根據啟動參數動態替換JNI實現。比如:類似于C庫,JNI實現為了性能起見,并沒有對調用者傳入的參數進行檢查。但是在調試階段,也許這種檢查是很必要的,幫助你盡早發現BUG。例如如果你使用IBM JDK,你可以指定JVM參數–Xcheck:jni,告訴JVM使用帶檢查的JNI實現。

            參考:
            http://java.sun.com/docs/books/jni/html/jniTOC.html
            posted @ 2011-02-18 10:59 愛上龍卷風 閱讀(8033) | 評論 (1)編輯 收藏

            2009年8月7日 #

            DLL中導出函數有兩種方式,即:dllexport與.def文件。
            dllexport方式是:在函數聲明中加上__declspec(dllexport);
            .def方式是:采用模塊定義(.def)文件聲明,(.def)文件為鏈接器提供了有關被鏈接程序的導出、屬性及其他方面的信息。
            (關于def文件,可以參考http://msdn.microsoft.com/en-us/library/d91k01sh(VS.80).aspx)
            對于這兩種方式,需要特別說明的是:
             第一、用.def文件導出的函數,其名稱是按我們的意愿定義的,而用__declspec(dellexport)導出時,會有相應的修飾名,具體的話,根據不同的編譯器其修飾名也不一樣。

             第二、__declspec(dllexport)定義的導出多用于同一編譯器的隱式鏈接(靜態調用),而.def導出函數可以確定導出的函數名不會因為不同的編譯器而不同,可用于其它開發工具的調用。

            有了上面的知識,我們再看JNI環境下的問題。

            JNI定義了關鍵字JNIEXPORT,用于實現DLL中函數的導出的。實際在JNI中,JNIEXPORT被定義為,#define JNIEXPORT __declspec(dllexport),也就是說JNI默認的導出函數使用dllexport方式。我們知道,使用使用dllexport方式產生的導出函數名會根據編譯器發生變化,在這種情況下,當Java程序通過Native接口調用DLL本地方法時,可能會發生找不到導出函數的問題。所以,在JNI的情況下,因此最好是定義一個.def文件來指明導出函數,以避免發生UnSatisfiedLinkedException錯誤 。




            posted @ 2009-08-07 22:44 愛上龍卷風 閱讀(2615) | 評論 (1)編輯 收藏

            2009年3月12日 #

            static關鍵字,有兩個作用:
            1) 作用于局部變量,定義該變量的存儲方式,就是我們常常說的靜態局部變量。
            2) 作用于用于模塊內聲明的變量和函數,用于指示其可見性。

            先談談變量和函數的可見性。在默認的情況下,模塊內聲明的變量和函數是全局可見的,如下:
            //Test1.cpp
            struct {  
                
            int m;
            } test;
            //Test2.cpp
            struct {  
                
            int m;
            } test;
            BUILD的結果會報"multiply defined symbols found"
            如果想避免這樣的錯誤,需要使用static關鍵字。

            不過好像如果你使用typedef,就不會出現這樣的錯誤。
            //Test1.cpp
            typedef struct {  
                
            int m;
            } test;
            //Test2.cpp
            typedef struct {  
                
            int m;
            } test;
            或者:
            //Test1.cpp
            typedef struct {  
                
            int m;
            } test;
            //Test2.cpp
            typedef int test;
            關于這一點,我想可能是typedef的作用域是限定在模塊內的,所以沒有這個問題。這個使用VC和使用GCC都沒問題,不過我沒有查到官方的文檔。

            最后一點,想說的是,關于static變量的生命周期。一般來說:static聲明的變量初始化,只是在程序運行的第一次被執行。不過有例外,那就是如果該變量定義在dll內,那么該變量的初始化工作是在dll被裝載時執行,在這種情況下,程序雖然只運行一次,但是該靜態變量可能會被初始化好幾次(與dll被裝載的次數有關). 這個問題本質上是:static聲明的變量的生命周期與包含它的組件相關。從這個意義上,我們可以說,static變量在不同的包含組件(EXE或DLL)上表現出不同行為,移植能力差,容易造成BUG,這樣的BUG也不容易發現。我們在開發程序的時候,不可能去假設該靜態變量是被包含在什么樣的組件里。我想,這也是一個不鼓勵使用static變量的一個重要原因。







            posted @ 2009-03-12 18:12 愛上龍卷風 閱讀(2121) | 評論 (2)編輯 收藏

            2009年2月18日 #

                   Windows Nativec++應用大量使用了DLL技術。"動態鏈接"這幾字指明了DLLs是如何工作的。對于常規的函數庫,鏈接器從中拷貝它需要的所有庫函數,并把確切的函數地址傳送給調用這些函數的程序。而對于DLLs,函數儲存在一個獨立的動態鏈接庫文件中。在創建Windows程序時,鏈接過程并不把DLLs文件鏈接到程序上。直到程 序運行并調用一個DLLs中的函數時,該程序才要求這個函數的地址。此時Windows才在DLLs中尋找被調用函數,并把它的地址傳送給調用程序。采用這種方法,DLLs達到了復用代碼的極限。

                  對于DLL, 關鍵一點是,所有run on windows system 的程序可以共用同一個DLL庫,從而達到最大限度的代碼復用。并且,由于DLL并不拷貝它需要的所有庫函數 , 這樣的話NativeC++程序 executable image size 會比較小。

                  modularity的角度,如果要在Java的應用里尋找相對應的DLL的概念,我們會自然地想到jar包。JAR包可以被 Class Loader動態裝載進JVM, 不過要幾點區別需要說明的是:

            第一、從本質上來講,JAR包是存在于磁盤上的一些data而已(JVM解釋執行),而DLLexecutable image。

            第二、Class Data Sharing (CDS)作為一個新的feature,Java5才被引入,其做法就是:把 system jar 文件打包成為"shared archive",這些"shared archive"會作為memory-mapped in文件存在,共享于不同的JVM 進程間,以減少JVMfootprint,加快Java應用的啟動時間。

                        值得一提的是:兩者都有所謂的HELL問題(JAR HELL vs DLL HELL),新老版本的兼容問題始終讓人頭疼。

            詳見解釋:

            http://en.wikipedia.org/wiki/DLL_hell

            http://en.wikipedia.org/wiki/JAR_hell#JAR_hell

                


            posted @ 2009-02-18 15:28 愛上龍卷風 閱讀(2574) | 評論 (2)編輯 收藏

            2009年1月12日 #

            題目是這樣的:
            問:不使用任何循環語句,遞歸,輸出打印n條(n>1) "Hello World"。
            解這道題目,利用了c++語言一個非常重要的特性:
            c++允許定義基于statck數據區的Object。由此,不由想到了Java.
            在Java的世界里,所有的類型都是引用(或者稱為指針), 對象內存的分配都是通過new從heap上顯式的分配,無法在Java里構建基于statck數據區的對象。所以在Java里,這道題目是無解的。
            Java之于c++,既是進步,又是倒退。
            Java語言本身的確幫助c++程序員做了很多事情,比如GC, 去掉了c++中很多復雜的特性,比如多重繼承,運算符重載等。
            同時,c++本身的很多優點,也喪失了。 寫了c++, 然后再寫java,一個明顯的感覺是,沒法使用java寫出像c++一樣簡潔的程序。
            比如, 沒有了運算符重載, 你不得不使用equal方法來表達兩個對象的相等。
            Java不能顯式表達RAII概念,你不得不使用hard code的方法Log方法的進入和退出, 如:
            func() {
            log("enter func");
            //do something.
            log("exit func");
            }

            posted @ 2009-01-12 22:28 愛上龍卷風 閱讀(2819) | 評論 (23)編輯 收藏

            2009年1月3日 #

            我在IBM dwWorks上發了一篇關于Monitor Object 并發設計模式的文章。

            Monitor Object 并發模式在 Java 同步機制中的實現。 里面用了很多C++的設計行為,來討論Java的Monitor Object.
            感興趣的話,歡迎訪問
            以下是摘要:

            文章將從兩個方面進行闡述:

            1. 使用 C++ 語言來描述 Monitor Object 設計模式。Java 對于這樣一個典型的模式做了很好的語言層面的封裝,因此對于 Java 的開發者來說,很多關于該模式本身的東西被屏蔽掉了。本文試圖使用 Native C++ 語言,幫助讀者從本質上對 Monitor object 設計模式有一個更全面的認識。
            2. 結合 C++ 版本的 Monitor Object 設計模式,引領讀者對于 Java 同步機制有一個更深刻的認識,幫助讀者正確有效地使用 Java 同步機制。


            posted @ 2009-01-03 18:26 愛上龍卷風 閱讀(2528) | 評論 (2)編輯 收藏

            2008年2月3日 #

                    一般來講, 在服務器上,如果有足夠的資源,Winsock server,理論上可以支持成千的并發連接。而現實是,我們沒有足夠的資源可供使用,分配。本文主要來討論一下內存資源之于Winsock server開發的重要性。
            一)基本概念。
            -> Pages,Locked Pages.
                    在現代操作系統中,內存管理會把主存(RAM)分成Pages來管理。 Paging(或者swapping)指的是主存與外存之間以Page為單位進行數據的交換。Locked Pages指的是被鎖定在主存中的內存頁,以保證一些內核組件,driver可以訪問到它們。windows一定會保證一定數量的可交換的內存空間,防止一些非法程序鎖定所有的物理內存,而致使系統崩潰。在windows NT, windows 2000上,可鎖定的內存總的大小上限大概是物理內存的1/8(當然對于程序的開發人員,不應該對這個值進行任何的假設,這個值可能會隨著操作系統本版的變化而變化)。在Winsock應用開發過程中,以overlapped方式讀寫IO操作,將會導致內存被鎖定。
            -> working set
                    在程序開始運行,并達到其穩定的運行狀態(主要指的是其對內存的使用),在這個狀態下,程序使用內存的數量一般小于其需要使用內存的總量。這樣一個穩定的運行狀態,我們可以稱為working set: 被該程序頻繁訪問的內存頁的集合。在windows上,你可以使用SetWorkingSetSize Win32 API來增加程序使用物理內存的數量。
            -> non-paged pool
                   不可交換的內存。這主要指以non-paged的方式分配的內存,這些內存就像locked pages一樣,是從來不會被交換出去的,用來存放一些由內核組件,driver訪問的信息。 在Winsock應用開發過程中,以下的操作可能導致分配non-paged內存。
            1) 調用系統一些系統的API,例如打開文件,create socket,等,都會導致從non-paged pool分配內存。
            2) 一些driver可以顯式地從該區域分配內存。
            二) Winsock server上Locked Pages使用。
                    我們提到過,任何的overlapped IO操作,都會導致鎖定內存頁。這些內存頁一旦被locked,就不會被交換出去。我們知道,windows操作系統對最大的可鎖定內存頁做了一個上限,如果超出這個上限,overlapped IO調用將會導致WSAENOBUFS錯誤。
                    考慮下面的情況,如果server在每個連接上會發出很多的overlapped receives操作,那么,隨著連接數目的增多,很明顯,被鎖定的內存數量很有可能達到上限而導致WSAENOBUFS錯誤。在這種情況下,如果服務器預期會處理大數量的客戶端連接,則需要服務器在每個連接上發出zero-byte buffer的overlapped接收請求(這種情況下,因為the size of buffer is zero,所以沒有任何內存被鎖定),一旦overlapped接收操作完成,server可以以non-blocking方式執行receive操作,以取得所有緩存在so_rcvbuf中的數據,直到返回WSAEWOULDBLOCK為止。
                    另外需要注意的是,windows在page的邊界上對內存進行鎖定,在x86平臺上,它是4kb的整數倍。所以,假如你post了一個1 KB buffer,而系統真實鎖定的是4 KB 的大小,為了避免這樣的浪費,盡量用4kb的整數做overlapped  IO操作。
            三) Winsock server上non-paged pool使用。
                    同Locked Pages限制一樣,windows對non-paged pool也有一個最大的限制。并且,當你的應用出現這個問題的時候,超出它的最大限制數,情況要遠比Locked Pages復雜。這種情況下,后果是不確定的,有可能你的Winsock調用返回WSAENOBUFS錯誤,也有可能,在系統中,一個和你的應用毫無關聯的driver由于申請不到non-paged內存而致使system crash。而這樣的災難,是沒法恢復的。
                    考慮一個具體的例子:我們假設在windows2000上,系統有1 GB內存。這樣的配置下,windows大概會預留1/4的空間用作non-paged pool(同樣,對于程序的開發人員,不應該對這個值進行任何的假設),即:256MB。這樣的配置下,保守估計,我們的Winsock server能夠處理到大概50,000連接,或者更多。(每個accepted socket大概消耗1.5kb,每個連接上post一個overlapped操作,分配一個IRP,大概需要500 byte, 總計:(1500+500)*50,000 = 100 Mb) 。
                   無論是對于Locked Pages,還是對于non-paged pool使用,一旦超出了上限,Winsock調用僅僅會返回一般的WSAENOBUFS 或者ERROR_INSUFFICIENT_RESOURCES錯誤。為了處理這些錯誤,你可以試試以下的方法:
            1) 需要首先調用SetWorkingSetSize,增加應用的可支配資源數,看能否解決。
            2)     確信你的應用沒有做出太多的overlapped  IO操作。
            3) 關閉一些連接數。
            四) SOCKET的緩沖區設置問題。

                     Winsock在默認的情況下,每個socket都會與一個send和receive buffer相關聯。你可以通過調用setsockopt來設置buffer的大小。
                    在緩沖區沒有被關閉的情況下,我們看看overlapped send和revc是怎么工作的。
                    當上層的應用做出了send調用,而這時如果send buffer還有剩余的空間,那么數據將會從用戶提交的buffer復制到send buffer中,然后調用返回成功。否則,假如這時send buffer已滿,用戶提交的buffer將會被鎖定,并且調用返回WSA_IO_PENDING。當send buffer的數據被下層的tcp處理完成,winsock將直接處理用戶提交的buffer里的數據,而不需要再復制。
                    同樣,對于recv操作,如果數據已經被緩存在socket的receive buffer里,當發生recv調用的時候,數據將直接從socket的receive buffer復制到用戶的buffer里,recv調用返回成功。否則,假如發生調用時receive buffer里沒有數據,用戶提交的buffer將會被鎖定,recv調用返回WSA_IO_PENDING。當數據到達當前連接,將會被直接復制到用戶提交的buffer里。
                    一個應用程序通過設定send buffer為0,把緩沖區關閉,然后發出一個阻塞send()調用。在這樣的情況下,系統內核會把應用程序的緩沖區鎖定,直到接收方確認收到了整個緩沖區后send()調用才返回。似乎這是一種判定你的數據是否已經為對方全部收到的簡潔的方法,實際上卻并非如此。想想看,即使遠端tcp通知數據已經收到,其實也根本不代表數據已經成功送給客戶端應用程序,比如對方可能發生資源不足的情況,導致afd.sys不能把數據拷貝給應用程序。另一個更要緊的問題是,在每個線程中每次只能進行一次發送調用,效率極其低下。
                    另外,希望通過關閉Winsock緩沖區,從而避免數據復制,達到優化性能的目的,也是不可取的。從上面,我們看到:只要應用保證適量的,足夠的send, recv調用,這樣的復制是完全可以避免的。
                    高性能的服務器應用程序可以關閉發送緩沖區,同時不會損失性能。不過,這樣的應用程序必須十分小心,保證它總是發出多個重疊發送調用,而不是等待某個重疊發送結束了才發出下一個。如果應用程序是按一個發完再發下一個的順序來操作,那浪費掉兩次發送中間的空檔時間,總之是要保證傳輸驅動程序在發送完一個緩沖區后,立刻可以轉向另一個緩沖區。
                    如果關閉了recv buffer,在你的應用沒有保證足夠的recv操作前提下,任何進來數據,必須在TCP層進行緩存,最大緩存的數量將取決于tcp windows的大小(17Kb)。而最為嚴重的是這些緩存是從non-paged pool分配而來。如上所述,non-paged pool是非常珍貴,稀缺的內存。所以,從這個意義上來講,關閉了recv buffer操作是不可取的。
                這是2007年最后一篇帖子,最后祝大家新年快樂,過個好年!??!

            posted @ 2008-02-03 15:18 愛上龍卷風 閱讀(3090) | 評論 (2)編輯 收藏

            2008年1月5日 #

            四、c++中的多態規則。
            一) c++中函數動態綁定規則。
            看下面的例子:

            class Window
            {

            public:
              virtual 
            void  oops()
              
            {
                cout
            <<"Window oops"<<endl;
              }

            public:
              
            int height;
              
            int width;
            }
            ;
            class TextWindow : public Window
            {

            public:
              virtual 
            void  oops()
              
            {
                cout
            <<"TextWindow oops"<<cursorLocation<<endl;
              }

            public:
              
            int cursorLocation;
            }
            ;

            main()
            {
              Window win;
              Window
            * tWin;
              TextWindow 
            * tWinPtr;

              tWinPtr 
            = new TextWindow;
              tWin 
            = tWinPtr;
              
              win.oops();
              tWin
            ->oops();
            }


            類TextWindow繼承與類Window。我想程序運行的結果,大多數熟悉C++的人都會知道,
            win.oops()最終調用的是父類oops函數,而tWin->oops()調用的是子類TextWindow的函數。
            通過這個例子,我們先總結一下c++中的多態調用規則
            第一、對于指針和引用類型,當消息調用的成員函數有可能被重寫時,最終被選擇調用的成員函數由消息接收者的動態類型確定(注意:在OO概念中,對某個對象成員函數進行調用,常常稱為給該對象發送消息,該對象就是消息的接收者)。
                  如上例:tWin->oops(),由于tWin的動態類型是子類TextWindow,而不是Windows,所以tWin->oops()調用的是子類TextWindow的函數。
            二、對于其它的變量,對虛擬函數調用綁定完全由該變量的靜態類型確定(即該變量的聲明),而不是該變量的真實類型確定。
                 如上例:win.oops(),由于win的聲明類型為Window類,所以其結果調用的是父類oops函數。
            二) 探討。
                 接下來,我們要看的問題是,在c++中,為什么對于多態規則(或者說是動態函數綁定規則),做出了兩中不同的劃分,即:只有指針與引用類型,才進行函數的后期動態綁定,也就是多態。這或許也是許多c++初學者非常迷惑的地方。這種規則的不一致性,的確給c++的語法造成一定的復雜性。而這在Java,或者C#中是沒有的,后面我們會涉及到。
                  我們先來看例子。 

            void f()
            {
              Window  win;
              Window
            * tWinPtr;
              
              tWinPtr 
            = new TextWindow;
              win     
            = *tWinPtr;//what's problem happen
               
              win.oops(); 
            //what's problem happen
              tWinPtr->oops();
            }


                  在這里,如果我們假設,c++的函數動態綁定規則是一致的,看看會發生什么問題???
                  現在win被聲明為Window類型,然而其真實的類型為TextWindow(因為win=*tWinPtr),由于我們的假設,win現在是允許進行動態函數綁定的,所以當執行win.oops()時,實際上是調用子類TextWindow的成員函數。
                 現在,我們有必要來審視一下win變量的內存布局。由于win變量是在棧上聲明的變量,其內存也是從棧進行分配(這是c++從c語言那里繼承過來的優良特質,從棧上分配內存空間比動態分配內存有更好的執行速度),c++標準規定:給win變量分配內存空間的大小,由其靜態的類型確定,即應該是Window類所使用的內存空間大小。在這種情況下,當執行win=*tWinPtr時,什么會發生?如下圖:

            在默認的拷貝構造函數情況下,信息會出現丟失,這就是著名的slicing off現象。結果,變量cursorLocation在win的內存空間里丟失了。然而,問題是:在我們假設下,我們要求win.oops()導致TextWindow的成員函數調用,而在這個函數中,訪問到的cursorLocation變量是不存在!win.oops()調用將導致內存違例!
                  到這里,我們可以總結一下:c++標準基于的其特定的內存分配規則,給出了以上,我們在前一節總結出的函數動態綁定規則。
            三) 深入。
                  當然,我們也可以說,c++也可以通過改變其內存分配規則,來給出一個一致性的函數動態綁定規則。比如:可以考慮在給win變量分配內存空間時,考慮其所有子類需求,然后分配最大數量的內存空間給win變量。這種做法可行性很差,對于編譯器而言,需要掃描整個程序(確定該類的所有子類),才能確定最大的內存空間是多少。在使用類庫或者框架的情況下,會引起整個類庫,框架的重新編譯,這是得不償失的!而這種做法,在oo的語言中,基本上是沒有的。這也是c++不得不基于其現有的內存管理機制,而對多態規則作出的不一致的解釋。
                對于這個c++現有的內存管理機制,我們如果從另外角度去理解的話,是很合理的。當win=*tWinPtr發生
            時,我們可以類似地認為:好比一個float類型的數賦給了一個interger類型的變量,其結果當然是float的值被截斷了。
                我們再來看其它語言,Java(或者C#)是怎么解決的。
                最重要的一點是,在Java(C#)中只有引用的概念,所以在棧上聲明的類的變量,只需要分配一個指針大小的內存空間就行了,而不需要給該變量分配空間來保存變量內容本身,這其實就是我們現在看到的c++中指針和引用的情況。
                

             

            posted @ 2008-01-05 23:12 愛上龍卷風 閱讀(3162) | 評論 (8)編輯 收藏

            2007年11月11日 #

                    C++之父Bjarne stroustrup曾經說過:不需要了解所有的c++細節,也能夠寫出好的c++程序;不應該注重語言方面的特征,而應該注重軟件設計技術本身。很顯然,我的這篇文章,與這兩句話背道而馳:).的確,我們程序員,不應該把精力放在c++本身語言的特征上,而是應該思考軟件設計技術本身。那么,在我們需要提高對c++理解的同時,是不是我們從下面幾個方面為著眼點
            1) 從編譯原理的角度
            2) 從技術需求的角度
            3) 從軟件設計技術的角度
            從以上的幾個角度,來重新審視c++一些晦澀語法,或許,我們能從中獲益。在這里,我要說的是,我們不單單是要記住這些c++語言特性怎么樣的使用,而是應該知道這些語言特性背后隱藏的故事,以便于我們更深層次地理解c++,理解軟件設計。
            一、子類通過函數名字隱藏父類函數。
            如下例:

            class Base
            {
            public:
             virtual 
            void f(int x);
            }
            ;
            class Derived: public Base
            {
            public:
             virtual 
            void f(double* pd);
            }
            ;
            int main()
            {
              Derived
            * pd = new Derived();
              pd
            ->f(10); //compile error!!!
            }

                     當我們編譯pd->f(10)操作時,編譯器報錯。按照我們常規的理解是:父類的函數void f(int x)與子類的函數void f(double*pd),由于參數類型不同,其函數簽名也是不一樣的,按照這樣的邏輯,在這個類繼承體系中,這兩個函數完全應該是互不隱藏的,我們完全可以認為是符合overloaded規則的兩個函數。
                    但是,在c++里,子類通過函數名字隱藏父類函數,而不是通過函數簽名!c++給出的解釋也是合理的:試想一種情況:你使用了別人寫的類庫,繼承其中的某個類,寫了你自己的子類。
            如上面的例子,你的子類就是Derived,而類庫中的父類就是Base.當你根本不知道在父類中還有這樣一個f(int x)函數時,在調用子類Derived的f函數時,你犯了錯誤,參數類型傳成了int類型(或者不是你犯的錯誤,編譯器幫你自動轉化為int類型),結果是:程序可以正常運行,但是,執行的結果卻不是你所期望的,是f(int x)調用,而不是你自己的實現:f(double* pd)調用!
                     這就是c++為什么通過函數名字隱藏父類函數的原因。
                    說到這里,我們需要補充幾句:雖然c++在語言層面上給我們提供了這樣的保證,但是,子類hide父類的函數,這是一個非常不好的設計。從OO的角度出發,應該講求的是Liskov Substitution Principle。即:suntypes must be substitutable fro their base types.很顯然,當hide行為發生時,從接口的角度來講,子類與父類是不能互為替代的。父類的protected or public的方法,應該很自然地由其所有子類所繼承,而不是被隱藏。隱藏行為的發生,相當于在這套繼承體系中開的一個后門。很顯然,C++幫助我們自動隱藏了父類的方法,但是,作為程序開發的我們,應該意識到這一點,也應該避免這樣的設計。
            二、c++的per-class allocator語法規則
                      在D&E of C++一書中,Stroustrup給出了幾點c++提供per-class allocator的理由,這些理由也是我們使用class level的allocator的原因,所以,有必要我們總結一下:
            第一、許多程序應用,需要在運行的過程中,大量地Create和Delete對象。這些對象,諸如:tree nodes,linked list nodes,messages等等。如果在傳統的heap完成這些對象的創建,銷毀,由于大量的內存申請,釋放,勢必會造成內存碎片。這種情況下,我們需要對內存分配進行細粒度的控制。
            第二、一些應用需要長時間跑在內存受限的裝置上,這也需要我們對內存分配進行細粒度的控制,而不是無限制地分配,釋放。
            主要基于以上的兩點,c++提供了per-class allocator語言支持。
            如下例:

            class X
            {
            public:
              
            void* operator new(size_t sz); //allocate sz bytes
              void  operator delete(void* p) //free p;
            }
            ;

                  new操作符函數負責對象X的內存分配。對這樣一個語法規則,我們好奇的是,為什么聲明了一個我們從來都不使用的參數size_t sz.我們的使用語法如下: X* px = new X;
            C++也給出了解釋:per-class allocator機制將適用整個類的繼承體系。例如:

            class Y: public X //ojects of class Y are also allocated using X::operator new
            {
              
            //
              
            // 
            }
            ;

                    對于子類Y,其內存分配函數也是X::operator new()。但是,在這里,內存分配的大小,不應該是sizeof(X),而是sizeof(Y).問題的關鍵在這里:C++通過提供多余的參數size_t sz,而給開發者提供了更大的靈活性,也即:per-class allocator是面向類的繼承體系的內存管理機制,而不單單是面向單個類。
            三、Koenig Lookup機制。
                    大家對Andrew Koenig應該很熟悉,c++大牛,是AT&T公司Shannon實驗室大規模編程研究部門中的成員,同時他也是C++標準委員會的項目編輯。他擁有超過30年的編程經驗,其中有15年的C++使用經驗。
                    Koenig Lookup,就是以Andrew Koenig命名的查找規則。在看這個定義之前,我們先弄清楚函數所在的域的分類,一般來講,分為:
            1:類域(函數作為某個類的成員函數(靜態或非靜態))
            2:名字空間域
            3:全局域(即C++默認的namespace)
                    而Koenig Lookup機制,就是當編譯器對無限定域的函數調用進行名字查找時,除了當前名字空間域以外,也會把函數參數類型所處的名字空間加入查找的范圍。
            如下例:

            #include <iostream>
            using namespace std;
            namespace Koenig
            {
                
            class MyArg
                
            {
                
            public:
                     ostream
            & print(ostream& out) const
                     
            {
                        out
            <<"this is MyArg."<<endl;
                     }

                }
            ;
             
                inline ostream
            & operator<<(ostream& out, const MyArg& myArg)
                
            {
                     
            return myArg.print(out);
                }

            }

             
            int main()
            {
                Koenig::MyArg myArg;
                cout
            <<myArg;
                
            return 0;
            }

                   如上的代碼,使用operator<<操作符函數,打印對象的狀態,但是函數ostream& operator<<(ostream& out, const MyArg& myArg) 的定義域是處于名字空間Koenig中,為什么編譯器在解析main函數(全局域)里面的operator<<調用時,它能夠正確定位到Koenig名字空間里面的operator<<?這是因為根據Koenig查找規則,編譯器需要把參數類型MyArg所在的名字空間Koenig也加入對ostream& operator<<(ostream& out, const MyArg& myArg) 調用的名字查找范圍中。
                   
            如果沒有Koenig查找規則,我們就無法直接寫cout<<myArg;,而是需要寫類似Koenig::operator<<(std::cout, myArg); 這樣的代碼(使用完全限定名)。這樣的結果是,即不直觀也不方便。

                    其實在C++里,提供了很多類似于Koenig查找規則的機制,以保證程序語法上的簡潔,明了。例如:許多的操作符函數,COPY構造函數。而這些,也是我們寫出專業的C++程序的基本。
            未完待續:)

             

             


             

            posted @ 2007-11-11 14:56 愛上龍卷風 閱讀(4625) | 評論 (26)編輯 收藏

            2007年8月26日 #

                     本文主要探討一下windows平臺上的完成端口開發及其與之相關的幾個重要的技術概念,這些概念都是與基于IOCP的開發密切相關的,對開發人員來講,又不得不給予足夠重視的幾個概念:
            1) 基于IOCP實現的服務吞吐量
            2)IOCP模式下的線程切換
            3)基于IOCP實現的消息的亂序問題。

            一、IOCP簡介
                提到IOCP,大家都非常熟悉,其基本的編程模式,我就不在這里展開了。在這里我主要是把IOCP中所提及的概念做一個基本性的總結。IOCP的基本架構圖如下:
             

            如圖所示:在IOCP中,主要有以下的參與者:
            --》完成端口:是一個FIFO隊列,操作系統的IO子系統在IO操作完成后,會把相應的IO packet放入該隊列。
            --》等待者線程隊列:通過調用GetQueuedCompletionStatus API,在完成端口上等待取下一個IO packet。
            --》執行者線程組:已經從完成端口上獲得IO packet,在占用CPU進行處理。
            除了以上三種類型的參與者。我們還應該注意兩個關聯關系,即:
            --》IO Handle與完成端口相關聯:任何期望使用IOCP的方式來處理IO請求的,必須將相應的IO Handle與該完成端口相關聯。需要指出的時,這里的IO Handle,可以是File的Handle,或者是Socket的Handle。
            --》線程與完成端口相關聯:任何調用GetQueuedCompletionStatus API的線程,都將與該完成端口相關聯。在任何給定的時候,該線程只能與一個完成端口相關聯,與最后一次調用的GetQueuedCompletionStatus為準。
            二、高并發的服務器(基于socket)實現方法
                    一般來講,實現基于socket的服務器,有三種實現的方式(thread per request的方式,我就不提了:)):
            第一、線程池的方式。使用線程池來對客戶端請求進行服務。使用這種方式時,當客戶端對服務器的連接是短連接(所謂的短連接,即:客戶端對服務器不是長時間連接)時,是可以考慮的。但是,如若客戶端對服務器的連接是長連接時,我們需要限制服務器端的最大連接數目為線程池線程的最大數目,而這應用的設計本身來講,是不好的設計方式,scalability會存在問題。
            第二、基于Select的服務器實現。其本質是,使用Select(操作系統提供的API)來監視連接是否可讀,可寫,或者是否出錯。相比于前一種方式,Select允許應用使用一個線程(或者是有限幾個線程)來監視連接的可讀寫性。當有連接可讀可寫時,應用可以以non-bolock的方式讀寫socket上的數據。使用Select的方式的缺點是,當Select所監視的連接數目在千的數量級時,性能會打折扣。這是因為操作系統內核需要在內部對這些Socket進行輪詢,以檢查其可讀寫性。另一個問題是:應用必須在處理完所有的可讀寫socket的IO請求之后,才能再次調用Select,進行下一輪的檢查,否則會有潛在的問題。這樣,造成的結果是,對一些請求的處理會出現饑餓的現象。
                    一般common的做法是Select結合Leader-Follower設計模式使用。不過不管怎樣,Select的本質造成了其在Scalability的問題是不如IOCP,這也是很多high-scalabe的服務器采用IOCP的原因。
            第三、IOCP實現高并發的服務器。IOCP是實現high-scalabe的服務器的首選。其特點我們專門在下一小姐陳述。
            三、IOCP開發的幾個概念
            第一、服務器的吞吐量問題。
                  我們都知道,基于IOCP的開發是異步IO的,也正是這一技術的本質,決定了IOCP所實現的服務器的高吞吐量。
                   我們舉一個及其簡化的例子,來說明這一問題。在網絡服務器的開發過程中,影響其性能吞吐量的,有很多因素,在這里,我們只是把關注點放在兩個方面,即:網絡IO速度與Disk IO速度。我們假設:在一個千兆的網絡環境下,我們的網絡傳輸速度的極限是大概125M/s,而Disk IO的速度是10M/s。在這樣的前提下,慢速的Disk 設備會成為我們整個應用的瓶頸。我們假設線程A負責從網絡上讀取數據,然后將這些數據寫入Disk。如果對Disk的寫入是同步的,那么線程A在等待寫完Disk的過程是不能再從網絡上接受數據的,在寫入Disk的時間內,我們可以認為這時候Server的吞吐量為0(沒有接受新的客戶端請求)。對于這樣的同步讀寫Disk,一些的解決方案是通過增加線程數來增加服務器處理的吞吐量,即:當線程A從網絡上接受數據后,驅動另外單獨的線程來完成讀寫Disk任務。這樣的方案缺點是:需要線程間的合作,需要線程間的切換(這是另一個我們要討論的問題)。而IOCP的異步IO本質,就是通過操作系統內核的支持,允許線程A以非阻塞的方式向IO子系統投遞IO請求,而后馬上從網絡上讀取下一個客戶端請求。這樣,結果是:在不增加線程數的情況下,IOCP大大增加了服務器的吞吐量。說到這里,聽起來感覺很像是DMA。的確,許多軟件的實現技術,在本質上,與硬件的實現技術是相通的。另外一個典型的例子是硬件的流水線技術,同樣,在軟件領域,也有很著名的應用。好像話題扯遠了,呵呵:)
            第二、線程間的切換問題。
                     服務器的實現,通過引入IOCP,會大大減少Thread切換帶來的額外開銷。我們都知道,對于服務器性能的一個重要的評估指標就是:System\Context Switches,即單位時間內線程的切換次數。如果在每秒內,線程的切換次數在千的數量級上,這就意味著你的服務器性能值得商榷。Context Switches/s應該越小越好。說到這里,我們來重新審視一下IOCP。
                 完成端口的線程并發量可以在創建該完成端口時指定(即NumberOfConcurrentThreads參數)。該并發量限制了與該完成端口相關聯的可運行線程的數目(就是前面我在IOCP簡介中提到的執行者線程組的最大數目)。當與該完成端口相關聯的可運行線程的總數目達到了該并發量,系統就會阻塞任何與該完成端口相關聯的后續線程的執行,直到與該完成端口相關聯的可運行線程數目下降到小于該并發量為止。最有效的假想是發生在有完成包在隊列中等待,而沒有等待被滿足,因為此時完成端口達到了其并發量的極限。此時,一個正在運行中的線程調用GetQueuedCompletionStatus時,它就會立刻從隊列中取走該完成包。這樣就不存在著環境的切換,因為該處于運行中的線程就會連續不斷地從隊列中取走完成包,而其他的線程就不能運行了。
                 完成端口的線程并發量的建議值就是你系統CPU的數目。在這里,要區分清楚的是,完成端口的線程并發量與你為完成端口創建的工作者線程數是沒有任何關系的,工作者線程數的數目,完全取決于你的整個應用的設計(當然這個不宜過大,否則失去了IOCP的本意:))。
            第三、IOCP開發過程中的消息亂序問題。
                 使用IOCP開發的問題在于它的復雜。我們都知道,在使用TCP時,TCP協議本身保證了消息傳遞的次序性,這大大降低了上層應用的復雜性。但是當使用IOCP時,問題就不再那么簡單。如下例:
              
                   三個線程同時從IOCP中讀取Msg1, Msg2,與Msg3。由于TCP本身消息傳遞的有序性,所以,在IOCP隊列內,Msg1-Msg2-Msg3保證了有序性。三個線程分別從IOCP中取出Msg1,Msg2與Msg3,然后三個線程都會將各自取到的消息投遞到邏輯層處理。在邏輯處理層的實現,我們不應該假定Msg1-Msg2-Msg3順序,原因其實很簡單,在Time 1~Time 2的時間段內,三個線程被操作系統調度的先后次序是不確定的,所以在到達邏輯處理層,
            Msg1,Msg2與Msg3的次序也就是不確定的。所以,邏輯處理層的實現,必須考慮消息亂序的情況,必須考慮多線程環境下的程序實現。
                    在這里,我把消息亂序的問題單列了出來。其實在IOCP的開發過程中,相比于同步的方式,應該還有其它更多的難題需要解決,這也是與Select方式相比,IOCP的缺點,實現復雜度高。
            結束語:
                ACE的Proactor Framework, 對windows平臺的IOCP做了基于Proactor設計模式的,面向對象的封裝,這在一定程度上簡化了應用開發的難度,是一個很好的異步IO的開發框架,推薦學習使用。 
            Reference:
                Microsoft Technet,Inside I/O Completion Ports

            posted @ 2007-08-26 16:06 愛上龍卷風 閱讀(16469) | 評論 (23)編輯 收藏

            僅列出標題  下一頁
            99久久人妻无码精品系列蜜桃| 18岁日韩内射颜射午夜久久成人| 国产精品成人久久久久久久| av午夜福利一片免费看久久| 精品久久久久久亚洲精品| 久久久久AV综合网成人| 国产精品久久久久久| 国产精品丝袜久久久久久不卡| 国产亚洲美女精品久久久| 久久本道综合久久伊人| 久久福利片| 91精品国产综合久久香蕉| 亚洲国产精品无码久久青草 | 中文字幕亚洲综合久久菠萝蜜| 品成人欧美大片久久国产欧美...| 91精品观看91久久久久久| 合区精品久久久中文字幕一区| 久久精品成人欧美大片| 91精品国产综合久久久久久| 久久91精品国产91久久小草| 一级a性色生活片久久无少妇一级婬片免费放| 99久久精品免费| 中文字幕精品久久久久人妻| 久久久无码一区二区三区| 久久精品国产精品亚洲艾草网美妙 | 精品一二三区久久aaa片| 91麻豆精品国产91久久久久久| 久久久久久av无码免费看大片 | 久久久久久久波多野结衣高潮| 久久久精品2019免费观看| 久久青青草原精品国产软件| 亚洲日本va中文字幕久久| 国内精品久久久久久久影视麻豆| 久久久久久国产a免费观看黄色大片| av无码久久久久久不卡网站| 国产69精品久久久久APP下载 | 久久久久久亚洲精品影院| 99久久综合国产精品二区| 精品国产一区二区三区久久久狼| 色99久久久久高潮综合影院| 国产欧美一区二区久久|