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

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