完成IO使用總結IOCP

http://blog.csdn.net/xionghaoaizhangruyun/article/details/8184863



IOCPI/O Completion PortI/O完成端口)是性能最好的一種I/O模型。它是應用程序使用線程池處理異步I/O請求的一種機制。在處理多個併發的異步I/O請求時,以往的模型都是在接收請求是創建一個線程來應答請求。這樣就有很多的線程並行地運行在系統中。而這些線程都是可運行的,Windows內核花費大量的時間在進行線程的上下文切換,並沒有多少時間花在線程運行上。再加上創建新線程的開銷比較大,所以造成了效率的低下。

 

調用的步驟如下:

抽象出一個完成端口大概的處理流程:

1:創建一個完成端口。

2:創建一個線程A

3A線程循環調用GetQueuedCompletionStatus()函數來得到IO操作結果,這個函數是個阻塞函數。

4:主線程循環裏調用accept等待客戶端連接上來。

5:主線程裏accept返回新連接建立以後,把這個新的套接字句柄用CreateIoCompletionPort關聯到完成端口,然後發出一個異步的WSASend或者WSARecv調用,因爲是異步函數,WSASend/WSARecv會馬上返回,實際的發送或者接收數據的操作由WINDOWS系統去做。

6:主線程繼續下一次循環,阻塞在accept這裏等待客戶端連接。

7WINDOWS系統完成WSASend或者WSArecv的操作,把結果發到完成端口。

8A線程裏的GetQueuedCompletionStatus()馬上返回,並從完成端口取得剛完成的WSASend/WSARecv的結果。

9:在A線程裏對這些數據進行處理(如果處理過程很耗時,需要新開線程處理),然後接着發出WSASend/WSARecv,並繼續下一次循環阻塞在GetQueuedCompletionStatus()這裏。

歸根到底概括完成端口模型一句話:

我們不停地發出異步的WSASend/WSARecvIO操作,具體的IO處理過程由WINDOWS系統完成,WINDOWS系統完成實際的IO處理後,把結果送到完成端口上(如果有多個IO都完成了,那麼就在完成端口那裏排成一個隊列)。我們在另外一個線程裏從完成端口不斷地取出IO操作結果,然後根據需要再發出WSASend/WSARecvIO操作。

 

IOCP模型是事先開好了N個線程,存儲在線程池中,讓他們hold。然後將所有用戶的請求都投遞到一個完成端口上,然後N個工作線程逐一地從完成端口中取得用戶消息並加以處理。這樣就避免了爲每個用戶開一個線程。既減少了線程資源,又提高了線程的利用率。

 

完成端口模型是怎樣實現的呢?我們先創建一個完成端口(::CreateIoCompletioPort())。然後再創建一個或多個工作線程,並指定他們到這個完成端口上去讀取數據。我們再將遠程連接的套接字句柄關聯到這個完成端口(還是用::CreateIoCompletionPort())。一切就OK了。

 

工作線程都幹些什麼呢?首先是調用::GetQueuedCompletionStatus()函數在關聯到這個完成端口上的所有套接字上等待I/O的完成。再判斷完成了什麼類型的I/O。一般來說,有三種類型的I/OOP_ACCEPTOP_READOP_WIRTE。我們到數據緩衝區內讀取數據後,再投遞一個或是多個同類型的I/O即可(::AcceptEx()::WSARecv()::WSASend())。對讀取到的數據,我們可以按照自己的需要來進行相應的處理。

 

爲此,我們需要一個以OVERLAPPED(重疊I/O)結構爲第一個字段的per-I/O數據自定義結構。

 

typedef struct _PER_IO_DATA

{

        OVERLAPPED ol;       //重疊I/O結構

        char buf[BUFFER_SIZE];   //數據緩衝區

        int nOperationType;         //I/O操作類型

#define OP_READ 1

#define OP_WRITE 2

#define OP_ACCEPT 3

} PER_IO_DATA, *PPER_IO_DATA;

 

將一個PER_IO_DATA結構強制轉化成一個OVERLAPPED結構傳給::GetQueuedCompletionStatus()函數,返回的這個PER_IO_DATA結構的的nOperationType就是I/O操作的類型。當然,這些類型都是在投遞I/O請求時自己設置的。

 

這樣一個IOCP服務器的框架就出來了。當然,要做一個好的IOCP服務器,還有考慮很多問題,如內存資源管理、接受連接的方法、惡意的客戶連接、包的重排序等等。以上是個人對於IOCP模型的一些理解與看法,還有待完善。另外各Winsock API的用法參見MSDN

 

 

完成端口中的單句柄數據結構與單IO數據結構的理解與設計

 

完成端口模型,針對於win平臺的其它異步網絡模型而言,最大的好處,除了性能方面的卓越外,還在於完成端口在傳遞網絡事件的通知時,可以一併傳遞與此事件相關的應用層數據。這個應用層數據,體現在兩個方面:一是單句柄數據,二是單io數據。

 

  getqueuedcompletionstatus函數的原型如下:

  winbaseapi

  bool

  winapi

  getqueuedcompletionstatus(

      in handle completionport,

      out lpdwordlpnumberofbytestransferred,

      out pulong_ptr lpcompletionkey,

      out lpoverlapped *lpoverlapped,

      in dword dwmilliseconds

     );

  其中,我們把第三個參數lpcompletionkey稱爲完成鍵,由它傳遞的數據稱爲單句柄數據。我們把第四個參數lpoverlapped稱爲重疊結構體,由它傳遞的數據稱爲單io數據。

 

  以字面的意思來理解,lpcompletionkey內包容的東西應該是與各個socket一一對應的,而lpoverlapped是與每一次的wsarecvwsasend操作一一對應的。

 

  在網絡模型的常見設計中,當一個客戶端連接到服務器後,服務器會通過acceptacceptex創建一個socket,而應用層爲了保存與此socket相關的其它信息(比如:該socket所對應的sockaddr_in結構體數據,該結構體內含客戶端ip等信息,以及爲便於客戶端的邏輯包整理而準備的數據整理緩衝區等),往往需要創建一個與該socket一一對應的客戶端底層通信對象,這個對象可以負責保存僅在網絡層需要處理的數據成員和方法,然後我們需要將此客戶端底層通信對象放入一個類似於listmap的容器中,待到需要使用的時候,使用容器的查找算法根據socket值找到它所對應的對象然後進行我們所需要的操作。

 

  讓人非常高興的是,完成端口“體貼入微”,它已經幫我們在每次的完成事件通知時,稍帶着把該socket所對應的底層通信對象的指針送給了我們,這個指針就是lpcompletionkey。也就是說,當我們從getqueuedcompletionstatus函數取得一個數據接收完成的通知,需要將此次收到的數據放到該socket所對應的通信對象整理緩衝區內對數據進行整理時,我們已經不需要去執行listmap等的查找算法,而是可以直接定位這個對象了,當客戶端連接量很大時,頻繁查表還是很影響效率的。哇哦,太帥了,不是嗎?呵呵。

 

  基於以上的認識,我們的lpcompletionkey對象可以設計如下:

  typedefstruct per_handle_data

  {

    socketsocket;             //本結構體對應的socket

    sockaddr_inaddr;          //用於存放客戶端ip等信息

    chardatabuf[ 2*max_buffer_size ];  //整理緩衝區,用於存放每次整理時的數據

  }

 

  per_handle_datasocket的綁定,通過createiocompletionport完成,將該結構體地址作爲該函數的第三個參數傳入即可。而per_handle_data結構體中addr成員,是在accept執行成功後進行賦值的。databuf則可以在每次wsarecv操作完成,需要整理緩衝區數據時使用。

 

  下面我們再來看看完成端口的收、發操作中所使用到的重疊結構體overlapped

 

  關於重疊io的知識,請自行google相關資料。簡單地說,overlapped是應用層與核心層交互共享的數據單元,如果要執行一個重疊io操作,必須帶有overlapped結構。在完成端口中,它允許應用層對overlapped結構進行擴展和自定義,允許應用層根據自己的需要在overlapped的基礎上形成新的擴展overlapped結構。一般地,擴展的overlapped結構中,要求放在第一個的數據成員是原overlapped結構。我們可以形如以下方式定義自己的擴展overlapped結構:

  typedefstruct per_io_data

  {

    overlappedovl;

    wsabuf           buf;

    char                    recvdatabuf[max_buffer_size ];   //接收緩衝區

    char                    senddatabuf[max_buffer_size ];   //發送緩衝區

    optype              optype;                                                      //操作類型:發送、接收或關閉等

  }

  

  在執行wsasendwsarecv操作時,應用層會將擴展overlapped結構的地址傳給核心,核心完成相應的操作後,仍然通過原有的這個結構傳遞操作結果,比如“接收”操作完成後,recvdatabuf裏存放便是此次接收下來的數據。

 

  根據各自應用的不同,不同的完成端口設計者可能會設計出不同的per_handle_data

per_io_data,我這裏給出的設計也只是針對自己的應用場合的,不一定就適合你。但我想,最主要的還是要搞明白per_handle_dataper_io_data兩種結構體的含義、用途,以及調用流程。

 

CRITICAL_SECTION理解的總結

 

很多人對CRITICAL_SECTION的理解是錯誤的,認爲CRITICAL_SECTION是鎖定了資源,其實,CRITICAL_SECTION是不能夠“鎖定”資源的,它能夠完成的功能,是同步不同線程的代碼段。簡單說,當一個線程執行了EnterCritialSection之後,cs裏面的信息便被修改了,以指明哪一個線程佔用了它。而此時,並沒有任何資源被“鎖定”。不管什麼資源,其它線程都還是可以訪問的(當然,執行的結果可能是錯誤的)。只不過,在這個線程尚未執行LeaveCriticalSection之前,其它線程碰到EnterCritialSection語句的話,就會處於等待狀態,相當於線程被掛起了。這種情況下,就起到了保護共享資源的作用。

     也正由於CRITICAL_SECTION是這樣發揮作用的,所以,必須把每一個線程中訪問共享資源的語句都放在EnterCritialSectionLeaveCriticalSection之間。這是初學者很容易忽略的地方。

當然,上面說的都是對於同一個CRITICAL_SECTION而言的。如果用到兩個CRITICAL_SECTION,比如說:

第一個線程已經執行了EnterCriticalSection(&cs)並且還沒有執行LeaveCriticalSection(&cs),這時另一個線程想要執行EnterCriticalSection(&cs2),這種情況是可以的(除非cs2已經被第三個線程搶先佔用了)。這也就是多個CRITICAL_SECTION實現同步的思想。

 

      比如說我們定義了一個共享資源dwTime[100],兩個線程ThreadFuncAThreadFuncB都對它進行讀寫操作。當我們想要保證dwTime[100]的操作完整性,即不希望寫到一半的數據被另一個線程讀取,那麼用CRITICAL_SECTION來進行線程同步如下:

第一個線程函數:

DWORD WINAPI ThreadFuncA(LPVOID lp)

{

EnterCriticalSection(&cs);

...

// 操作dwTime

...

LeaveCriticalSection(&cs);

return 0;

}

寫出這個函數之後,很多初學者都會錯誤地以爲,此時csdwTime進行了鎖定操作,dwTime處於cs的保護之中。一個“自然而然”的想法就是——csdwTime一一對應上了。這麼想,就大錯特錯了。dwTime並沒有和任何東西對應,它仍然是任何其它線程都可以訪問的。

 

如果你像如下的方式來寫第二個線程,那麼就會有問題:

DWORD WINAPI ThreadFuncB(LPVOID lp)

{

...

// 操作dwTime

...

return 0;

}

當線程ThreadFuncA執行了EnterCriticalSection(&cs),並開始操作dwTime[100]的時候,線程ThreadFuncB可能隨時醒過來,也開始操作dwTime[100],這樣,dwTime[100]中的數據就被破壞了。

     爲了讓CRITICAL_SECTION發揮作用,我們必須在訪問dwTime的任何一個地方都加上EnterCriticalSection(&cs)LeaveCriticalSection(&cs)語句。所以,必須按照下面的方式來寫第二個線程函數:

DWORD WINAPI ThreadFuncB(LPVOID lp)

{

EnterCriticalSection(&cs);

...

// 操作dwTime

...

LeaveCriticalSection(&cs);

return 0;

}

      這樣,當線程ThreadFuncB醒過來時,它遇到的第一個語句是EnterCriticalSection(&cs),這個語句將對cs變量進行訪問。如果這個時候第一個線程仍然在操作dwTime[100]cs變量中包含的值將告訴第二個線程,已有其它線程佔用了cs。因此,第二個線程的EnterCriticalSection(&cs)語句將不會返回,而處於掛起等待狀態。直到第一個線程執行了LeaveCriticalSection(&cs),第二個線程的EnterCriticalSection(&cs)語句纔會返回,並且繼續執行下面的操作。

       這個過程實際上是通過限制有且只有一個函數進入CriticalSection變量來實現代碼段同步的。簡單地說,對於同一個CRITICAL_SECTION,當一個線程執行了EnterCriticalSection而沒有執行LeaveCriticalSection的時候,其它任何一個線程都無法完全執行EnterCriticalSection而不得不處於等待狀態。

再次強調一次,沒有任何資源被“鎖定”,CRITICAL_SECTION這個東東不是針對於資源的,而是針對於不同線程間的代碼段的!我們能夠用它來進行所謂資源的“鎖定”,其實是因爲我們在任何訪問共享資源的地方都加入了EnterCriticalSectionLeaveCriticalSection語句,使得同一時間只能夠有一個線程的代碼段訪問到該共享資源而已(其它想訪問該資源的代碼段不得不等待)。

如果是兩個CRITICAL_SECTION,就以此類推。

 

再舉個極端的例子,可以幫助你理解CRITICAL_SECTION這個東東:

第一個線程函數:

DWORD WINAPI ThreadFuncA(LPVOID lp)

{

EnterCriticalSection(&cs);

for(int i=0;i <1000;i++)

Sleep(1000);

LeaveCriticalSection(&cs);

return 0;

}

 

第二個線程函數:

DWORD WINAPI ThreadFuncB(LPVOID lp)

{

EnterCriticalSection(&cs);

index=2;

LeaveCriticalSection(&cs);

return 0;

}

      這種情況下,第一個線程中間總共Sleep1000秒鐘!它顯然沒有對任何資源進行什麼“有意識”的保護;而第二個線程是要訪問資源index的,但是由於第一個線程佔用了cs,一直沒有Leave,而導致第二個線程不得不登上1000秒鐘……

第二個線程,真是可憐哪。。。

這個應該很說明問題了,你會看到第二個線程在1000秒鐘之後開始執行index=2這個語句。

也就是說,CRITICAL_SECTION其實並不理會你關心的具體共享資源,它只按照自己的規律辦事~

 

 

函數模板

 

函數模板不是一個可以執行的函數,它只是對函數功能的程序描述,編譯程序不爲它生成執行代碼。函數模板的聲明格式是:

 

template <class 類型參數名1, class 類型參數名 2,>

函數返回值類型函數名(形參表)

{

  函數體;

}

 

下面舉例說明創建和應用函數模板。

 

【例12-2-1】編寫一個程序,使它能夠輸出不同類型數組中的數據。

 

一般情況下,我們會使用函數重載的方法實現:

 

參考源代碼:

 

/* 12-2-112-2-1_1.cpp */

 

#include <iostream.h>

 

#include <stdlib.h>

 

using namespace std;

 

void outputArray(int *array, int count)

 

{

 

  for ( int i = 0; i < count; i++ )

 

  cout << array[i] << " ";

 

  cout << endl;

 

 }

 

void outputArray(double *array, int count)

 

{

 

  for ( int i = 0; i < count; i++ )

 

  cout << array[i] << " ";

 

  cout << endl;

 

}

 

void outputArray(char *array, int count)

 

{

 

  for ( int i = 0; i < count; i++ )

 

  cout << array[i] << " ";

 

  cout << endl;

 

}

 

void main()

 

{

 

  const int ac = 5, bc = 3, cc = 5;

 

  int a[ac] = { 1, 2, 3, 4, 5 };

 

  double b[bc] = { 5.1, 2.7, 4.9 };

 

  char c[cc] = "SCIT";

 

  cout << "Array a: ";

 

  outputArray(a, ac);             //輸出數組a

 

  cout << "Array b: ";

 

  outputArray(b, bc);             //輸出數組b

 

  cout << "Array c:" ;

 

  outputArray(c, cc);        //輸出數組c

 

  system("pause");

 

}

 

運行結果:

 

Array a: 1      2    3     4     5

 

Array b: 5.1   2.7 4.9

 

Array c: S      C    I      T

 

引入函數模板後,我們就可以做如下修改:

 

第一步,寫出其中的一個普通函數:

 

void outputArray(int *array,int count)

 

{

 

 for ( int i =0; i < count; i++ )

 

 cout <<array[i] << " ";

 

 cout <<endl;

 

}

 

第二步,將數據類型參數化,即把其中具體的數據類型名全部替換成由自己定義的抽象的類型參數名(T)

 

// 將原來的int改爲T,注意,用類型參數替換的是數據類型名,不是變量名

 

void outputArray(T *array,int count)

 

{

 

  for (int  i = 0; i < count; i++ )

 

  cout<< array[i] << " ";

 

  cout<< endl;

 

}

 

第三步,使用關鍵字template聲明類型參數名,並放在函數頭前:

 

 template<class T>      //行末不要加分號!

 

 voidoutputArray(T *array,int count)

 

 {

 

   for ( int i= 0; i < count; i++ )

 

   cout<< array[i] << " ";

 

   cout<< endl;

 

 }

 

這樣我們就把一個普通的函數修改成一個通用的函數模板。那麼把它應用到【例12-2-1】,就可以寫成如下程序,程序執行的輸出與上同:

 

參考源代碼:

 

/* 12-2-112-2-1_2.cpp */

 

#include <iostream.h>

 

#include <stdlib.h>

 

using namespace std;

 

template <class T>        //定義數組的通用函數模板outputArray()

 

void outputArray(T *array, int count)

 

{

 

  for( int i = 0; i < count; i++ )

 

 cout << array[i] << " ";

 

 cout << endl;

 

}

 

void main()

 

{

 

  const int ac = 5, bc = 3, cc = 5;

 

  int a[ac] = { 1, 2, 3, 4, 5, }; //數組初始化

 

  double b[bc] = { 5.1, 2.7, 4.9 };

 

  char c[cc] = "SCIT";

 

  cout << "Array a:"<< endl;

 

  outputArray(a, ac);      //輸出數組a

 

  cout << "Array b:"<< endl;

 

  outputArray(b,bc);      //輸出數組b

 

  cout << "Array c:" << endl;

 

  outputArray(c, cc);      //輸出數組c

 

  system("pause");

 

}  

 

改寫後,可以看到,通過創建一個數組的通用函數模板outputArray(),其類型參數名爲T(代替了intdoublechar等普通的數據類型),系統就能根據主函數中函數調用的第一個實參的數據類型,由函數模板生成不同數據類型的函數。

 

現在,讓我們瞭解模板函數的概念,以及函數模板與模板函數的關係。

 

通過前面的學習,我們已經瞭解到函數模板僅是對一組函數的描述,是一個函數模型,而不是一個實實在在的函數,只有當編譯系統在程序中發現有與函數模板中相匹配的函數調用時生成一系列的重載函數,重載函數的函數體與函數模板的函數體相同。

 

 

WSAStartup(

 應用程序或DLL在使用Windows Sockets服務之前必須要進行一次成功的WSAStartup()調用.當它完成了Windows Sockets的使用後,應用程序或DLL必須調用WSACleanup()將其從Windows Sockets的實現中註銷,並且該實現釋放爲應用程序或DLL分配的任何資源.任何打開的並已建立連接的SOCK_STREAM類型套接口在調用WSACleanup()時會重置;而已經由closesocket()關閉卻仍有要發送的懸而未決數據的套接口則不會受影響-該數據仍要發送.

  對應於一個任務進行的每一次WSAStartup()調用,必須有一個WSACleanup()調用.只有最後的WSACleanup()做實際的清除工作;前面的調用僅僅將Windows Sockets DLL中的內置引用計數遞減.一個簡單的應用程序爲確保WSACleanup()調用了足夠的次數,可以在一個循環中不斷調用WSACleanup()直至返回WSANOTINITIALISED.

  返回值:

  0操作成功.

  SOCKET_ERROR否則.同時可以調用WSAGetLastError()獲得錯誤代碼.


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