http://blog.csdn.net/ithzhang/article/details/8508161 轉載請注明出處!!
???????????? IO 完成端口
為了將 Windows 打造成一個出色的服務器環境, Microsoft 開發出了 IO 完成端口。完成端口需要與線程池配合使用。
完成端口背后的理論是并發運行的線程數量必須有一個上限。由于太多的線程將會導致系統花費很大的代價在各個線程 cpu 上下文進行切換。
使用并發模型與創建進程相比開銷要低很多,但是也需要為每個客戶請求創建一個新的線程。這開銷仍然很大。通過使用線程池可以是性能有很大的提高。 IO 完成端口需要配合線程池配合使用。
IO 完成端口也是一個內核對象。調用以下函數創建 IO 完成端口內核對象。
HANDLE CreateIoCompletionPort( HANDLE hFile, HANDLE hExistingCompletionPort, ULONG_PTR CompletionKey, DWORD dwNumberOfConcurrentThreads);
這個函數會完成兩個任務:
一是創建一個 IO 完成端口對象。
二是將一個設備與一個 IO 完成端口關聯起來。
hFile 就是設備句柄。
hExistingCompletionPort 是與設備關聯的 IO 完成端口句柄。為 NULL 時,系統會創建新的完成端口。
dwCompletionKey 是一個對我們有意義的值,但是操作系統并不關心我們傳入的值。一般用它來區分各個設備。
dwNumberOfConcurrentThreads 告訴 IO 完成端口在同一時間最多能有多少進程處于可運行狀態。如果傳入 0 ,那么將使用默認值(并發的線程數量等于 cpu 數量)。在第二章我們曾介紹說幾乎所有的內核對象都需要安全屬性參數。那時的幾乎就是因為 IO 完成端口這個例外。它是唯一一個不需要安全屬性的內核對象。這是因為 IO 完成端口在設計時就是只在一個進程中使用。
每次調用 CreateIoCompletionPort 時,函數會判斷 hExistingCompletionKey 是否為 NULL ,如果為 NULL ,會創建新的完成端口內核對象。并為此完成端口創建設備列表然后將設備加入到此完成端口設備列表中(先入先出)。
設備列表存儲與該完成端口相關聯的所有設備。
設備列表只是調用 CreateIoCompletionPort 函數時的一個數據結構。除此之外還有四個結構。
第二個結構是 IO 完成隊列。當設備的一個異步 IO 請求完成時,系統會檢查該設備是否與一個完成端口相關聯,如果關聯,系統會將這個已完成的 IO 請求添加到完成端口的 IO 完成隊列中。每一項包括已傳輸字節數,完成鍵( dwCompletionKey )值,以及一個指向 IO 請求的 OVERLAPPED 結構指針和錯誤碼。
Windows 為 IO 完成端口提供了一個函數,可以將線程切換到睡眠狀態,來等待設備 IO 請求完成并進入完成端口。
BOOL GetQueuedCompletionStatus( HANDLE hCompletionPort, PDWORD pdwNumberOfBytesTransferred, ULONG_PTR pCompletionKey, OVERLAPPED** ppOverlapped, DWORD dwMilliSeconds);
hCompletionPort 表示線程希望對哪個完成端口進行監視, GetQueuedCompletionStatus 的任務就是將調用線程切換到睡眠狀態,也就是阻塞在此函數上,直到指定的 IO 完成端口出現一項或者超時。
pdwNumberOfBytesTransferred返回在異步IO完成時傳輸的字節數。
pCompletionKey返回完成鍵。
ppOverlapped返回異步IO開始時傳入的OVERLAPPED結構地址。
dwMillisecond指定等待時間。
函數執行成功則返回true,否則返回false。
第三個結構是等待線程隊列。當線程池中的每個線程調用 GetQueuedCompletionStatus 時,調用線程的線程標識符會被添加到這個等待線程隊列,這使得 IO 完成端口對象能知道,有哪些線程當前 正在等待 對已完成的 IO 請求進行處理。當 IO 完成端口的 IO 完成隊列中 出現一項時,完成端口會喚醒 等待線程隊列 中的一個線程。這個線程會得到已完成 IO 項的所有信息:已傳輸字節數,完成鍵以及 OVERLAPPED 結構地址。這些信息是通過傳給 GetQueuedCompletionStatus 的參數來返回的。
IO 完成隊列中的各項是以先入先出方式來進行的。但是喚醒等待隊列中的線程是按照后入先出的方式進行。假設有四個線程正在等待隊列中等待,如果出現了一個已完成的 IO 項,那么最后一個由于調用 GetQueuedCompletionStatus 而被掛起的線程會被喚醒來處理這一項。當處理完該項后,線程會由于再次調用 GetQueuedCompletionStatus 而進入等待線程隊列。使用這種算法,系統可以將哪些長時間睡眠的線程換出到磁盤。
作者一直在推崇 IO 完成端口。接下來我們來討論下為什么 IO 完成端口這么有用!!!
前面我們提到過 IO 完成端口只有配合線程池才能發揮更大的作用。當我們創建并關聯設備時,需要指定有多少個線程并發運行。一般將這個值設置為 cpu 的數量。當已完成的 IO 項被添加到完成隊列中時, IO 完成端口會喚醒正在等待的線程,但是喚醒的線程數最多不會超過我們指定的數量。如果有四個 IO 請求已完成,且有四個線程等待 GetQueuedCompletionStatus 而被掛起,那么 IO 完成端口只喚醒兩個線程處理,另外兩個線程繼續睡眠。此時讀者可能會疑問:既然完成端口只允許喚醒指定數量的線程,那么為什么還指定更多的線程在線程池中呢?這就涉及到 IO 完成端口的第四個數據結構:已釋放線程列表。它存儲已被喚醒的線程句柄。這使得 IO 完成端口能夠直到哪些線程已經被喚醒并監視它們的執行情況。如果此時已釋放線程由于調用某些函數將線程切換到了等待狀態,完成端口會將其從已釋放隊列中移除,并將其添加到已暫停線程列表。
已暫停隊列是 IO 完成端口第五個數據結構。
完成端口的目標是根據創建完成端口時指定的并發線程數量,將盡可能多的線程保持在已釋放線程列表中。如果一個已暫停的線程被喚醒,它會離開已暫停線程列表并重新進入已釋放線程列表。 這意味著已釋放列表中的線程數量將大于最大允許的并發線程數量。 這句話什么意思呢?正在運行的線程數量加上從暫停線程列表中被釋放的線程數量使總數大于最大允許的數量。這可以使線程數量在短時間內超過指定數量。
IO 完成端口并不一定要用于設備 IO ,它還可以進行線程間通信。在可提醒 IO 中我們介紹了 QueueUserAPC 。該函數允許線程將一個 APC 項添加到另一個線程的隊列中。 IO 完成端口也存在一個類似的函數:
>IO 通知追加到 IO 完成端口?size:14pt">
BOOL PostQueuedCompletionStatus( HANDLE hCompletionPort, DWORD dwNumBytes, ULONG_PTR CompletionKey, OVERLAPPED*pOverlapped);
這個函數用來將已完成的 IO 通知追加到 IO 完成端口的隊列中。
hCompletionPort 表示我們要將已完成的 IO 項添加到哪個完成端口的隊列中。
剩下的三個參數表示應該返回給主調線程的值。
?每個線程都調用一次 GetQueuedCompletionStatus ,將它們都喚?E7??程通信。例如:當用戶終止服務程序時,我們想要所有線程退出。如果各個線程還在等待完成端口但有沒有已完成的 IO 請求,那么無法將它們喚醒。我們可以為線程池中的每個線程都調用一次 GetQueuedCompletionStatus ,將它們都喚醒。各線程對 GetQueuedCompletionStatus 函數返回值進行檢查,如果發現應用程序正在終止,就會正常退出。(由于線程等待隊列是以棧方式喚醒各線程,為了保證線程池中每個線程都有機會得到模擬 IO 項,我們還必須在程序中采用其他線程同步機制)
當對完成端口調用 CloseHandle 時,系統會將所有正在等待 GetQueuedCompletionStatus 返回的線程喚醒,并返回 false 。 GetLastError 返回 ERROR_INVALID_HANDLE ,此時線程就可以知道應該退出了。
void CIOCompletionPortDlg::OnBnClickedBtnChoosefile() { // TODO: 在此添加控件通知處理程序代碼 CFileDialog dlg(true); dlg.DoModal(); m_fileName=dlg.GetPathName(); SetDlgItemText(IDC_EDIT_FILENAME,m_fileName); m_hSrcFile=CreateFile(m_fileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_FLAG_OVERLAPPED,NULL); if(m_hSrcFile==INVALID_HANDLE_VALUE) { return ; } DWORD filesize; DWORD filesizeHigh; m_SrcFileSize=GetFileSize(m_hSrcFile,&filesizeHigh); DWORD t=m_SrcFileSize/1024.0; //filesize/=1024.0; CString temp; temp.Format(TEXT("%d KB"),t); SetDlgItemText(IDC_EDIT_FILESIZE,temp); }
void CIOCompletionPortDlg::OnBnClickedBtnCopy() { // TODO: 在此添加控件通知處理程序代碼 m_hCopyThread=CreateThread(NULL,0,CopyThread,this,0,NULL); //CloseHandle(m_hCopyThread); }
//新線程入口函數:
DWORD WINAPI CIOCompletionPortDlg::CopyThread( PVOID ppram )
{
CIOCompletionPortDlg *pdlg=(CIOCompletionPortDlg*)ppram;
pdlg->m_hDesFile=CreateFile(TEXT("備份.exe"),GENERIC_WRITE,0,NULL,CREATE_ALWAYS,FILE_FLAG_OVERLAPPED,pdlg->m_hSrcFile);
LARGE_INTEGER filesize;
filesize.HighPart=0;
filesize.LowPart=pdlg->m_SrcFileSize;
SetFilePointerEx(pdlg->m_hDesFile,filesize,NULL,FILE_BEGIN);
SetEndOfFile(pdlg->m_hDesFile);
//創建IO完成端口。創建一個完成端口,將兩個設備將關聯到此完成端口上。
HANDLE hIOCP=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,4);//創建完成端口,但不關聯設備。
if(hIOCP==NULL)
{
return 0;
}
CreateIoCompletionPort(pdlg->m_hSrcFile,hIOCP,READ_KEY,0);//與IO完成端口關聯。
CreateIoCompletionPort(pdlg->m_hDesFile,hIOCP,WRITE_KEY,0);//在關聯時傳入了完成鍵。可以根據完成鍵來區別從完成隊列中取出的請求屬于哪個設備。
OVERLAPPED ov={0};
PostQueuedCompletionStatus(hIOCP,0,WRITE_KEY,&ov);//發送模擬完成異步IO消息。
BYTE *pBuffer=new BYTE[BUFFERSIZE];
OVERLAPPED ovDes={0};
OVERLAPPED ovSrc={0};
while(true)
{
//memset(pBuffer,0,sizeof(pBuffer));
DWORD nTransfer;
OVERLAPPED *overlapped;
ULONG_PTR CompletionKey;
GetQueuedCompletionStatus(hIOCP,&nTransfer,&CompletionKey,(OVERLAPPED**)&overlapped,INFINITE);//IO完成隊列沒有請求項則掛起。否則從IO完成隊列取出。
switch(CompletionKey)
{
case READ_KEY://從IO完成端口取出讀完成。
{ BOOL r=WriteFile(pdlg->m_hDesFile,pBuffer,overlapped->InternalHigh,NULL,&ovDes);
ovDes.Offset+=BUFFERSIZE;
}
break;
case WRITE_KEY://從IO完成隊列中取出寫完成。
{
memset(pBuffer,0,BUFFERSIZE);
if(ovSrc.Offset<pdlg->m_SrcFileSize)
{
DWORD nBytes;
if(ovSrc.Offset+BUFFERSIZE<pdlg->m_SrcFileSize)
nBytes=BUFFERSIZE;
else
nBytes=pdlg->m_SrcFileSize-ovSrc.Offset;
ReadFile(pdlg->m_hSrcFile,pBuffer,nBytes,NULL,&ovSrc);//異步IO忽略文件指針。所有對文件的定位操作由OVERLAPPED結構指定。
//一定要注意為每次異步IO請求提供一個OVERLAPPED結構。剛才由于在接收和發送使用了
//同一個OVERLAPPED結構,導致出現重疊 I/O 操作在進行中。錯誤代碼:997
ovSrc.Offset+=BUFFERSIZE;//OVERLAPPED的OffsetHigh結構必須每次都得設置。
}
else
{
::MessageBox(NULL,TEXT("文件復制完成"),TEXT(""),MB_OK);
return 0;
}
}
break;
default:
break;
}
}
return 0;
}
運行結果:
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
