一 基本概念
設備---windows操做系統上容許通訊的任何東西,好比文件、目錄、串行口、並行口、郵件槽、命名管道、無名管道、套接字、控制檯、邏輯磁盤、物理 磁盤等。絕大多數與設備打交道的函數都是CreateFile/ReadFile/WriteFile等。因此咱們不能看到**File函數就只想到文件 設備。html
與設備通訊有兩種方式,同步方式和異步方式。同步方式下,當調用ReadFile函數時,函數會等待系統執行完所要求的工做,而後才返回;異步方式下,ReadFile這類函數會直接返回,系統本身去完成對設備的操做,而後以某種方式通知完成操做。ios
重疊I/O----顧名思義,當你調用了某個函數(好比ReadFile)就馬上返回作本身的其餘動做的時候,同時系統也在對I/0設備進行你要求的操 做,在這段時間內你的程序和系統的內部動做是重疊的,所以有更好的性能。因此,重疊I/O是用於異步方式下使用I/O設備的。算法
重疊I/O須要使用的一個很是重要的數據結構OVERLAPPED。編程
完成端口---是一種WINDOWS內核對象。完成端口用於異步方式的重疊I/0狀況下,固然重疊I/O不必定非使用完成端口不可,還有設備內核對象、事 件對象、告警I/0等。可是完成端口內部提供了線程池的管理,能夠避免反覆建立線程的開銷,同時能夠根據CPU的個數靈活的決定線程個數,並且可讓減小 線程調度的次數從而提升性能。windows
二 OVERLAPPED數據結構
typedef struct _OVERLAPPED
{
ULONG_PTR Internal;//被系統內部賦值,用來表示系統狀態
ULONG_PTR InternalHigh;// 被系統內部賦值,傳輸的字節數
union {
struct
{
DWORD Offset;//和OffsetHigh合成一個64位的整數,用來表示從文件頭部的多少字節開始
DWORD OffsetHigh;//操做,若是不是對文件I/O來操做,則必須設定爲0
};
PVOID Pointer;
};
HANDLE hEvent;//若是不使用,就務必設爲0,不然請賦一個有效的Event句柄
} OVERLAPPED, *LPOVERLAPPED;api
下面是異步方式使用ReadFile的一個例子
OVERLAPPED Overlapped;
Overlapped.Offset=345;
Overlapped.OffsetHigh=0;
Overlapped.hEvent=0;
//假定其餘參數都已經被初始化
ReadFile(hFile,buffer,sizeof(buffer),&dwNumBytesRead,&Overlapped);
這樣就完成了異步方式讀文件的操做,而後ReadFile函數返回,由操做系統作本身的事情吧數組
下面介紹幾個與OVERLAPPED結構相關的函數
等待重疊I/0操做完成的函數
BOOL GetOverlappedResult (
ANDLE hFile,
LPOVERLAPPED lpOverlapped,//接受返回的重疊I/0結構
LPDWORD lpcbTransfer,//成功傳輸了多少字節數
BOOL fWait //TRUE只有當操做完成才返回,FALSE直接返回,若是操做沒有完成,經過調//用GetLastError ( )函數會返回ERROR_IO_INCOMPLETE
);緩存
宏HasOverlappedIoCompleted能夠幫助咱們測試重疊I/0操做是否完成,該宏對OVERLAPPED結構的Internal成員進行了測試,查看是否等於STATUS_PENDING值。安全
三 完成端口的內部機制
建立完成端口
完成端口是一個內核對象,使用時他老是要和至少一個有效的設備句柄進行關聯,完成端口是一個複雜的內核對象,建立它的函數是:
HANDLE CreateIoCompletionPort(
IN HANDLE FileHandle,
IN HANDLE ExistingCompletionPort,
IN ULONG_PTR CompletionKey,
IN DWORD NumberOfConcurrentThreads );
一般建立工做分兩步:
第一步,建立一個新的完成端口內核對象,可使用下面的函數:
HANDLE CreateNewCompletionPort(DWORD dwNumberOfThreads)
{
return CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,NULL,dwNumberOfThreads);
};
第二步,將剛建立的完成端口和一個有效的設備句柄關聯起來,可使用下面的函數:
bool AssicoateDeviceWithCompletionPort(HANDLE hCompPort,HANDLE hDevice,DWORD dwCompKey)
{
HANDLE h=CreateIoCompletionPort(hDevice,hCompPort,dwCompKey,0);
return h==hCompPort;
};
說明
1) CreateIoCompletionPort函數也能夠一次性的既建立完成端口對象,又關聯到一個有效的設備句柄
2) CompletionKey是一個能夠本身定義的參數,咱們能夠把一個結構的地址賦給它,而後在合適的時候取出來使用,最好要保證結構裏面的內存不是分配在棧上,除非你有十分的把握內存會保留到你要使用的那一刻。
3) NumberOfConcurrentThreads一般用來指定要容許同時運行的的線程的最大個數。一般咱們指定爲0,這樣系統會根據CPU的個數來自動肯定。
建立和關聯的動做完成後,系統會將完成端口關聯的設備句柄、完成鍵做爲一條紀錄加入到這個完成端口的設備列表中。若是你有多個完成端口,就會有多個對應的設備列表。若是設備句柄被關閉,則表中自動刪除該紀錄。
完成端口線程的工做原理
完成端口能夠幫助咱們管理線程池,可是線程池中的線程須要咱們使用_beginthreadex來建立,憑什麼通知完成端口管理咱們的新線程呢?答案在函數GetQueuedCompletionStatus。該函數原型:
BOOL GetQueuedCompletionStatus(
IN HANDLE CompletionPort,
OUT LPDWORD lpNumberOfBytesTransferred,
OUT PULONG_PTR lpCompletionKey,
OUT LPOVERLAPPED *lpOverlapped,
IN DWORD dwMilliseconds
);
這個函數試圖從指定的完成端口的I/0完成隊列中抽取紀錄。只有當重疊I/O動做完成的時候,完成隊列中才有紀錄。凡是調用這個函數的線程將被放入到完成端口的等待線程隊列中,所以完成端口就能夠在本身的線程池中幫助咱們維護這個線程。
完 成端口的I/0完成隊列中存放了當重疊I/0完成的結果---- 一條紀錄,該紀錄擁有四個字段,前三項就對應GetQueuedCompletionStatus函數的二、三、4參數,最後一個字段是錯誤信息 dwError。咱們也能夠經過調用PostQueudCompletionStatus模擬完成了一個重疊I/0操做。
當I/0完成隊列中出現 了紀錄,完成端口將會檢查等待線程隊列,該隊列中的線程都是經過調用GetQueuedCompletionStatus函數使本身加入隊列的。等待線程 隊列很簡單,只是保存了這些線程的ID。完成端口會按照後進先出的原則將一個線程隊列的ID放入到釋放線程列表中,同時該線程將從等待 GetQueuedCompletionStatus函數返回的睡眠狀態中變爲可調度狀態等待CPU的調度。
基本上狀況就是如此,因此咱們的線程要想成爲完成端口管理的線程,就必需要調用
GetQueuedCompletionStatus函數。出於性能的優化,實際上完成端口還維護了一個暫停線程列表,具體細節能夠參考《Windows高級編程指南》,咱們如今知道的知識,已經足夠了。
線程間數據傳遞
線程間傳遞數據最經常使用的辦法是在_beginthreadex函數中將參數傳遞給線程函數,或者使用全局變量。可是完成端口還有本身的傳遞數據的方法,答案就在於CompletionKey和OVERLAPPED參數。
CompletionKey 被保存在完成端口的設備表中,是和設備句柄一一對應的,咱們能夠將與設備句柄相關的數據保存到CompletionKey中,或者將 CompletionKey表示爲結構指針,這樣就能夠傳遞更加豐富的內容。這些內容只能在一開始關聯完成端口和設備句柄的時候作,所以不能在之後動態改 變。
OVERLAPPED參數是在每次調用ReadFile這樣的支持重疊I/0的函數時傳遞給完成端口的。咱們能夠看到,若是咱們不是對文件設 備作操做,該結構的成員變量就對咱們幾乎毫無做用。咱們須要附加信息,能夠建立本身的結構,而後將OVERLAPPED結構變量做爲咱們結構變量的第一個 成員,而後傳遞第一個成員變量的地址給ReadFile函數。由於類型匹配,固然能夠經過編譯。當GetQueuedCompletionStatus函 數返回時,咱們能夠獲取到第一個成員變量的地址,而後一個簡單的強制轉換,咱們就能夠把它看成完整的自定義結構的指針使用,這樣就能夠傳遞不少附加的數據 了。太好了!只有一點要注意,若是跨線程傳遞,請注意將數據分配到堆上,而且接收端應該將數據用完後釋放。咱們一般須要將ReadFile這樣的異步函數 的所須要的緩衝區放到咱們自定義的結構中,這樣當GetQueuedCompletionStatus被返回時,咱們的自定義結構的緩衝區變量中就存放了 I/0操做的數據。
CompletionKey和OVERLAPPED參數,均可以經過GetQueuedCompletionStatus函數得到。
線程的安全退出
不少線程爲了避免止一次的執行異步數據處理,須要使用以下語句
while (true)
{
....
GetQueuedCompletionStatus(...);
}
那麼如何退出呢,答案就在於上面曾提到的PostQueudCompletionStatus函數,咱們能夠用它發送一個自定義的包含了OVERLAPPED成員變量的結構地址,裏面包含一個狀態變量,當狀態變量爲退出標誌時,線程就執行清除動做而後退出。