完成端口加線程池技術實現_第1頁
完成端口加線程池技術實現_第2頁
完成端口加線程池技術實現_第3頁
完成端口加線程池技術實現_第4頁
完成端口加線程池技術實現_第5頁
已閱讀5頁,還剩6頁未讀 繼續免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

1/1完成端口加線程池技術實現WinSock異步I/O模型[5]完成端口-CompletionPort

如果你想在Windows平臺上構建服務器應用,那么I/O模型是你必須考慮的。Windows操作系統提供了五種I/O模型,分別是:

■選擇(select);

■異步選擇(WSAAsyncSelect);

■事件選擇(WSAEventSelect);

■重疊I/O(OverlappedI/O);

■完成端口(CompletionPort)。

每一種模型適用于一種特定的應用場景。程序員應該對自己的應用需求非常明確,綜合考慮到程序的擴展性和可移植性等因素,作出自己的選擇。

==============================================

█“完成端口”模型是迄今為止最復雜的一種I/O模型。但是,若一個應用程序同時需要管理很多的套接字,

那么采用這種模型,往往可以達到最佳的系統性能!但缺點是,該模型只適用于WindowsNT和Windows2000以上版本的操作系統。

█因其設計的復雜性,只有在你的應用程序需要同時管理數百乃至上千個套接字的時候,而且希望隨著系統內安裝的CPU數量的增多,

應用程序的性能也可以線性提升,才應考慮采用“完成端口”模型。

█從本質上說,完成端口模型要求我們創建一個Win32完成端口對象,通過指定數量的線程,

對重疊I/O請求進行管理,以便為已經完成的重疊I/O請求提供服務。

█※※※大家可以這樣理解,一個完成端口其實就是一個完成I/O的通知隊列,由操作系統把已經完成的重疊I/O請求的通知放入這個隊列中。

當某項I/O操作一旦完成,某個可以對該操作結果進行處理的工線程就會收到一則通知,工線程再去做一些其他的善后工作,

比如:將收到的數據進行顯示,等等。而套接字在被創建后,可以在任何時候與某個完成端口進行關聯。※※※

通常情況下,我們會在應用程序中創建一定數量的工線程來處理這些通知。線程數量取決于應用程序的特定需要。理想的情況是,線程數量等于處理器的數量,不過這也要求任何線程都不應該執行諸如同步讀寫、等待事件通知等阻塞型的操作,以免線程阻塞。每個線程都將分到一定的CPU時間,在此期間該線程可以運行,然后另一個線程將分到一個時間片并開始執行。如果某個線程執行了阻塞型的操作,操作系統將剝奪其未使用的剩余時間片并讓其它線程開始執行。也就是說,前一個線程沒有充分使用其時間片,當發生這樣的情況時,應用程序應該準備其它線程來充分利用這些時間片。

█使用這種模型之前,首先要創建一個I/O完成端口對象,用它面向任意數量的套接字句柄,管理多個I/O請求。

要做到這一點,需要調用CreateCompletionPort函數,其定義如下:

HANDLEWINAPICreateIoCompletionPort(

__inHANDLEFileHandle,

__inHANDLEExistingCompletionPort,

__inULONG_PTRCompletionKey,

__inDWORDNumberOfConcurrentThreads

);

要注意該函數有兩個功能:

●用于創建一個完成端口對象;

●將一個句柄同完成端口對象關聯到一起。

如果僅僅為了創建一個完成端口對象,唯一注意的參數便是NumberOfConcurrentThreads(并發線程的數量),前面三個參數可忽略。

NumberOfConcurrentThreads參數的特殊之處在于,它定義了在一個完成端口上,同時允許執行的線程數量。

理想情況下,我們希望每個處理器各自負責一個線程的運行,為完成端口提供服務,避免過于頻繁的線程“場景”(即線程上下文)切換。

若將該參數設為0,表明系統內安裝了多少個處理器,便允許同時運行多少個工線程!可用下述代碼創建一個I/O完成端口:

HANDLECompletionPort=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);

★1、工線程與完成端口

成功創建一個完成端口后,便可開始將套接字句柄與其關聯到一起。但在關聯套接字之前,首先必須創建一個或多個“工線程”,

以便在I/O請求投遞給完成端口后,為完成端口提供服務。在這個時候,大家或許會覺得奇怪,到底應創建多少個線程,以便為完成端口提供服務呢?

在此,要記住的一點,我們調用CreateIoComletionPort時指定的并發線程數量,與打算創建的工線程數量相比,它們代表的不是同一件事情。

CreateIoCompletionPort函數的NumberOfConcurrentThreads參數明確指示系統:

在一個完成端口上,一次只允許n個工線程運行。假如在完成端口上創建的工線程數量超出n個,那么在同一時刻,最多只允許n個線程運行。

但實際上,在一段較短的時間內,系統有可能超過這個值,但很快便會把它減少至事先在CreateIoCompletionPort函數中設定的值。

那么,為何實際創建的工線程數量有時要比CreateIoCompletionPort函數設定的

多一些呢?這樣做有必要嗎?

這主要取決于應用程序的總體設計情況。假定我們的某個工線程調用了一個函數,比如Sleep或WaitForSingleObject,

進入了暫停(鎖定或掛起)狀態,那么允許另一個線程代替它的位置。換言之,我們

希望隨時都能執行盡可能多的線程;

當然,最大的線程數量是事先在CreateIoCompletonPort調用里設定好的。這樣一來,假如事先預計到自己的線程有可能暫時處于停頓狀態,

那么最好能夠創建比CreateIoCompletonPort的NumberOfConcurrentThreads參數的值多的線程,以便到時候充分發揮系統的潛力。

==========================================

每一種模型適用于一種特定的應用場景。程序員應該對自己的應用需求非常明確,

綜合考慮到程序的擴展性和可移植性等因素,作出自己的選擇。

==============================================

█“完成端口”模型是迄今為止最復雜的一種I/O模型。但是,若一個應用程序同時

需要管理很多的套接字,

那么采用這種模型,往往可以達到最佳的系統性能!但缺點是,該模型只適用于WindowsNT和Windows2000以上版本的操作系統。

█因其設計的復雜性,只有在你的應用程序需要同時管理數百乃至上千個套接字的時候,而且希望隨著系統內安裝的CPU數量的增多,

應用程序的性能也可以線性提升,才應考慮采用“完成端口”模型。

█從本質上說,完成端口模型要求我們創建一個Win32完成端口對象,通過指定數量的線程,

對重疊I/O請求進行管理,以便為已經完成的重疊I/O請求提供服務。

█※※※大家可以這樣理解,一個完成端口其實就是一個完成I/O的通知隊列,由操作系統把已經完成的重疊I/O請求的通知放入這個隊列中。

當某項I/O操作一旦完成,某個可以對該操作結果進行處理的工線程就會收到一則通知,工線程再去做一些其他的善后工作,

比如:將收到的數據進行顯示,等等。而套接字在被創建后,可以在任何時候與某個完成端口進行關聯。※※※

通常情況下,我們會在應用程序中創建一定數量的工線程來處理這些通知。線程數量取決于應用程序的特定需要。理想的情況是,線程數量等于處理器的數量,不過這也要求任何線程都不應該執行諸如同步讀寫、等待事件通知等阻塞型的操作,以免線程阻塞。每個線程都將分到一定的CPU時間,在此期間該線程可以運行,然后另一個線程將分到一個時間片并開始執行。如果某個線程執行了阻塞型的操作,操作系統將剝奪其未使用的剩余時間片并讓其它線程開始執行。也就是說,前一個線程沒有充分使用其時間片,當發生這樣的情況時,應用程序應該準備其它線程來充分利用這些時間片。

█使用這種模型之前,首先要創建一個I/O完成端口對象,用它面向任意數量的套接字句柄,管理多個I/O請求。

要做到這一點,需要調用CreateCompletionPort函數,其定義如下:

HANDLEWINAPICreateIoCompletionPort(

__inHANDLEFileHandle,

__inHANDLEExistingCompletionPort,

__inULONG_PTRCompletionKey,

__inDWORDNumberOfConcurrentThreads

);

要注意該函數有兩個功能:

●用于創建一個完成端口對象;

●將一個句柄同完成端口對象關聯到一起。

如果僅僅為了創建一個完成端口對象,唯一注意的參數便是NumberOfConcurrentThreads(并發線程的數量),前面三個參數可忽略。

NumberOfConcurrentThreads參數的特殊之處在于,它定義了在一個完成端口上,同時允許執行的線程數量。

理想情況下,我們希望每個處理器各自負責一個線程的運行,為完成端口提供服務,避免過于頻繁的線程“場景”(即線程上下文)切換。

若將該參數設為0,表明系統內安裝了多少個處理器,便允許同時運行多少個工線程!可用下述代碼創建一個I/O完成端口:

HANDLECompletionPort=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);

★1、工線程與完成端口

成功創建一個完成端口后,便可開始將套接字句柄與其關聯到一起。但在關聯套接字之前,首先必須創建一個或多個“工線程”,

以便在I/O請求投遞給完成端口后,為完成端口提供服務。在這個時候,大家或許會覺得奇怪,到底應創建多少個線程,以便為完成端口提供服務呢?

在此,要記住的一點,我們調用CreateIoComletionPort時指定的并發線程數量,與打算創建的工線程數量相比,它們代表的不是同一件事情。

CreateIoCompletionPort函數的NumberOfConcurrentThreads參數明確指示系統:

在一個完成端口上,一次只允許n個工線程運行。假如在完成端口上創建的工線程數量超出n個,那么在同一時刻,最多只允許n個線程運行。

但實際上,在一段較短的時間內,系統有可能超過這個值,但很快便會把它減少至事

先在CreateIoCompletionPort函數中設定的值。

那么,為何實際創建的工線程數量有時要比CreateIoCompletionPort函數設定的

多一些呢?這樣做有必要嗎?

這主要取決于應用程序的總體設計情況。假定我們的某個工線程調用了一個函數,比如Sleep或WaitForSingleObject,

進入了暫停(鎖定或掛起)狀態,那么允許另一個線程代替它的位置。換言之,我們

希望隨時都能執行盡可能多的線程;

當然,最大的線程數量是事先在CreateIoCompletonPort調用里設定好的。這樣一來,假如事先預計到自己的線程有可能暫時處于停頓狀態,

那么最好能夠創建比CreateIoCompletonPort的NumberOfConcurrentThreads參數的值多的線程,以便到時候充分發揮系統的潛力。

==========================================

一旦在完成端口上擁有足夠多的工線程來為I/O請求提供服務,便可著手將套接

字句柄同完成端口關聯到一起。

這要求我們在一個現有的完成端口上,調用CreateIoCompletionPort函數,同時為前

三個參數—FileHandle,ExistingCompletionPort和CompletionKey—提供套接字的信息。

●FileHandle參數指定一個要同完成端口關聯在一起的套接字句柄;

●ExistingCompletionPort參數指定的是一個現有的完成端口;

●CompletionKey(完成鍵)參數指定與某個套接字句柄關聯在一起的“單句柄數據”,可將其作為指向一個數據結構的指針,

在此數據結構中,同時包含了套接字的句柄,以及與套接字有關的其他信息,如IP地址等。為完成端口提供服務的線程函數可通過這個參數,取得與套接字句柄有關的信息。

根據目前,首先來構建一個基本的應用程序框架。下面的程序清單向

大家闡述了如何使用完成端口模型,來開發一個服務器應用。在這個程序中,

我們按照以下步驟進行:

1)創建一個完成端口,第四個參數保持為0,指定在完成端口上,每個處理器一次只允許執行一個工線程;

2)判斷系統內到底安裝了多少個處理器;

3)創建工線程,根據步驟2)得到的處理器信息,在完成端口上,為已完成的I/O請求提供服務,在這個簡單的例子中,我們為每個處理器都只創建一個工線程。

這是由于事先已預計到,到時不會有任何線程進入“掛起”狀態,造成由于線程數量的不足,而使處理器空閑的局面(沒有足夠的線程可供執行)。

調用CreateThread函數時,必須同時提供一個工例程,由線程在創建好執行;

4)準備好一個監聽套接字,在端口9527上監聽進入的連接請求;

5)使用accept函數,接受進入的連接請求;

6)創建一個數據結構,用于容納“單句柄數據”,同時在結構中存入接受的套接字句柄;

7)調用CreateIoCompletionPort函數,將從accept返回的新套接字句柄同完成端口關聯到一起,

通過完成鍵(CompletionKey)參數,將單句柄數據結構傳遞給CreateIoCompletionPort函數;

8)開始在已接受的連接上進行I/O操作,在此,我們希望通過重疊I/O機制,在新建的套接字上投遞一個或多個異步WSARecv或WSASend請求。

這些I/O請求完成后,一個工線程會為I/O請求提供服務,同時繼續處理未來的其他I/O請求,

稍后便會在步驟3)指定的工例程中,體驗到這一點;

9)重復步驟5)~8),直至服務器中止。

代碼如下:

HANDLECompletionPort;

WSADATAwsd;

SYSTEM_INFOSystemInfo;

SOCKADDR_INInternetAddr;

SOCKETListen;

inti;

typedefstruct_PER_HANDLE_DATA

{

SOCKETSocket;

SOCKADDR_STORAGEClientAddr;

//Otherinformationusefultobeassociatedwiththehandle}PER_HANDLE_DATA,*LPPER_HANDLE_DATA;

//LoadWinsock

StartWinsock(MAKEWORD(2,2),

//Step1:

//創建一個完成端口

CompletionPort=CreateIoCompletionPort(

INVALID_HANDLE_VALUE,NULL,0,0);

//Step2:

//判斷系統內到底安裝了多少個處理器

GetSystemInfo(

//Step3:

//根據處理器的數量創建工線程

for(i=0;iSocket=Accept;

memcpy(

//Step7:

//調用CreateIoCompletionPort函數,將從accept返回的新套接字句柄同完成端口關聯到一起

CreateIoCompletionPort((HANDLE)Accept,

CompletionPort,(DWORD)PerHandleData,0);

//Step8:

//開始在已接受的連接上進行I/O操作

WSARecv(...);

}

DWORDWINAPIServerWorkerThread(LPVOIDlpParam)

{

//Therequirementsfortheworkerthreadwillbe

//discussedlater.

return0;

}

★2、完成端口和重疊I/O(工線程要做的事情)

將套接字句柄與一個完成端口關聯在一起后,便可投遞發送與接收請求,開始對I/O請求的處理。

接下來,可開始依賴完成端口,來接收有關I/O操作完成情況的通知。

從本質上說,完成端口模型利用了Win32重疊I/O機制。在這種機制中,象WSASend和WSARecv這樣的WinsockAPI調用會立即返回。

此時,需要由我們的應用程序負責在以后的某個時間,通過一個OVERLAPPED結構,來接收之前調用請求的結果。

在完成端口模型中,要想做到這一點,需要使用GetQueuedCompletionStatus(獲取

排隊完成狀態)函數,

讓一個或多個工線程在完成端口上等待I/O請求完成的通知。該函數的定義如下:BOOLWINAPIGetQueuedCompletionStatus(

__inHANDLECompletionPort,

__outLPDWORDlpNumberOfBytes,

__outPULONG_PTRlpCompletionKey,

__outLPOVERLAPPED*lpOverlapped,

__inDWORDdwMilliseconds

);

●CompletionPort參數對應于要在上面等待的完成端口;

●lpNumberOfBytes參數負責在完成了一次I/O操作后(如:WSASend或WSARecv),接收實際傳輸的字節數。

●lpCompletionKey參數為原先傳遞給CreateIoCompletionPort函數第三個參數“單句柄數據”,如我們早先所述,大家最好將套接字句柄保存在這個“鍵”(Key)中。

●lpOverlapped參數用于接收完成I/O操作的重疊結果。這實際是一個相當重要的參數,因為可用它獲取每個I/O操作的數據。

●dwMilliseconds參數用于指定希望等待一個完成數據包在完成端口上出現的時間,即,超時時間。假如將其設為INFINITE,會一直等待下去。

★3、“單句柄數據”和單I/O操作數據

一個工線程從GetQueuedCompletionStatus函數接收到I/O完成通知后,在lpCompletionKey和lpOverlapped參數中,

會包含一些重要的套接字信息。利用這些信息,可通過完成端口,繼續在一個套接字

上進行其他的處理。

通過這些參數,可獲得兩方面重要的套接字數據:“單句柄數據”以及單I/O操作數據。

其中,lpCompletionKey參數包含了“單句柄數據”,因為在一個套接字首次與完成端口關聯到一起的時候,

那些數據便與一個特定的套接字句柄對應起來了。這些數據正是我們在調用CreateIoCompletionPort函數時候,通過CompletionKey參數傳遞的。

通常情況下,應用程序會將與I/O請求有關的套接字句柄及其他的一些相關信息保存在這里;

lpOverlapped參數則包含了一個OVERLAPPED結構,在它后面跟隨“單I/O操作數據”。

單I/O操作數據可以是追加到一個OVERLAPPED結構末尾的、任意數量的字節。

假如一個函數要求用到一個OVERLAPPED結構,我們便必須將這樣的一個結構傳遞進去,以滿足它的要求。

要想做到這一點,一個簡單的方法是定義一個結構,然后將OVERLAPPED結構作為新結構的第一個元素使用。

舉個例子來說,可定義下述數據結構,實現對單I/O操作數據的管理:

typedefstruct

{

OVERLAPPEDOverlapped;

WSABUFDataBuf;

charszBuffer[DATA_BUF_SIZE];

intOperationType;

}PER_IO_OPERATION_DATA;

該結構演示了通常與I/O操作關聯的一些重要的數據元素,比如剛才完成的那個I/O操作的類型(發送或接收請求),用OperationType字段表示,

同時,用于已完成I/O操作數據的緩沖區szBuffer也是非常有用的。如果想調用一個WinsockAPI函數(如:WSASend、WSARecv),要為其分配一個OVERLAPPED結構,

這時,就可以將我們的結構強制轉換成一個OVERLAPPED指針,或者從結構中將OVERLAPPED元素的地址取出來。如下例所示:

PER_IO_OPERATION_DATAPerIoData;

……

//可以這樣調用:

WSARecv(socket,...,(OVERLAPPED*)

//也可以這樣調用:

WSARecv(socket,...,

在工作線程的后面部分,等GetQueuedCompletionStatus函數返回了一個重疊結構(和完成鍵)后,

便可通過OperationType成員,看出到底是哪個操作投遞到了這個句柄之上(只需將

返回的重疊結強制轉換為自己的PER_IO_OPERATION_DATA結構)。

對單I/O操作數據來說,它最大的一個優點便是允許我們在同一個句柄上,同時管理

多個I/O操作(讀/寫,多個讀,多個寫,等等)。

DWORDWINAPIServerWorkerThread(LPVOIDCompletionPortID)

{

HANDLECompletionPort=(HANDLE)CompletionPortID;

DWORDBytesTransferred;

LPOVERLAPPEDOverlapped;

LPPER_HANDLE_DATAPerHandleData;

LPPER_IO_DATAPerIoData;

DWORDSendBytes,RecvBytes;

DWORDFlags;

while(TRUE)

{

//WaitforI/Otocompleteonanysocket

//associatedwiththecompletionport

ret=GetQueuedCompletionStatus(CompletionPort,

//Firstchecktoseeifanerrorhasoccurred

//onthesocket;ifso,closethe

//socketandcleanuptheper-handledata

//andper-I/Ooperationdataassociatedwith

//thesocket

if(BytesTransferred==0

GlobalFree(PerHandleData);

GlobalFree(PerIoData);

continue;

}

//ServicethecompletedI/Orequest.Youcan

//determinewhichI/Orequesthasjust

//completedbylookingattheOperationType

//fieldcontainedintheper-I/Ooperationdata.

if(PerIoData->OperationType==RECV_POSTED){

//Dosomethingwiththereceiveddata

//inPerIoData->Buffer

}

//PostanotherWSASendorWSARecvoperation.//Asanexample,wewillpostanotherWSARecv()//I/Ooperation.

Flags=0;

//Setuptheper-I/Ooperationdataforthenext//overlappedcall

ZeroMemory(

PerIoData->DataBuf.len=DATA_BUFSIZE;

PerIoData->DataBuf.buf=PerIoData->Buffer;

PerIoData->OperationType=RECV_POSTED;

WSARecv(PerHandleData->Socket,

}

★4、正確地關閉I/O完成端口

如何正確地關閉I/O完成端口,特別是同時運行了一個或多個線程,在幾個不同的套

接字上執行I/O操作的時候。

要避免的一個重要問題是在進行重疊I/O操作的同時,強行釋放一個OVERLAPPED結構。

要想避免出現這種情況,最好的辦法是針對每個套接字句柄,調用closesocket函數,任何尚未進行的重疊I/O操作都會完成。一旦所有套接字句柄都已關閉,

便需在完成端口上,終止所有工線程的運行。要想做到這一點,需要使用PostQueuedCompletionStatus函數,向每個工線程都發送一個特殊的完成數據包。

該函數會指示每個線程都“立即結束并退出”。下面是PostQueuedCompletionStatus函數的定義:

BOOLWINAPIPostQueuedCompletionStatus(

__inHANDLECompletionPort,

__inDWORDdwNumberOfBytesTransferred,

__inULONG_PTRdwCompletionKey,

__inLPOVERLAPPEDlpOverlapped

);

●CompletionPort參數指定想向其發送一個完成數據包的完成端口對象;

●而就dwNumberOfBytesTransferred、dwCompletionKey和lpOverlapped三個參數

來說,每一個都允許我們指定一個值,

直接傳遞給GetQueuedCompletionStatus函數中對應的參數。這樣一來,一個工線程收到傳遞過來的三個GetQueuedCompletionStatus函數參數后,

便可根據由這三個參數的某一個設置的特殊值,決定何時或者應該怎樣退出。

例如,可用dwCompletionPort參數傳遞0值,而一個工線程會將其解釋成中止

指令。

一旦所有工線程都已關閉,便可使用CloseHandle函數,關閉完成端口,最終安

全退出程序。

==========================================

1、線程池的基本原理

在傳統服務器架構中,常常是有一個總的監聽線程監聽有沒有新的用戶連接服務器,每當有一個新的用戶連接進入,服務器端就開啟一個新的線程去處理這個用戶的請求,與其進行數據的收發。

這個線程只服務于這個用戶,當用戶與服務器端關閉連接以后,服務器端才銷毀這個線程。然而頻繁地開辟與銷毀線程極大地占用了系統的資源。而且在大量用戶的情況下,少則1000,多則上萬,系統為了開辟和銷毀線程將浪費大量的時間和資源。

線程池技術很好的解決了這個問題,它的基本思想就是在程序開始時就在內存中開辟一些線程,當有新的客戶請求到達時,

不是新創建一個線程為其服務,而是從“池子”中選擇一個空閑的線程為新的客戶請求服務,服務完畢后,線程不是退出,而是進入空閑線程池中。

通過對多個任務重用已經存在的線程對象,降低了對線程對象創建和銷毀的開銷。當客戶請求時,線程對象已經存在,可以提高請求的響應時間,從而整體地提高了系統服務的表現。

2、線程池的實現

如果大家有時間的話,可以自己實現一個高效的線程池,當然網上也有很多版本的線程池源代碼,大家可

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論