標(biāo) 題: Windows系統(tǒng)編程之異步I/O和完成端口 作 者: 北極星2003 時 間: 2006-07-02 18:46 鏈 接: http://bbs.pediy.com/showthread.php?threadid=28342 詳細(xì)信息: Windows系統(tǒng)編程之異步I/O和完成端口 【作者】北極星2003 【來源】看雪技術(shù)論壇(bbs.pediy.com) 【時間】2006年7月1日
一、 同步I/O和異步I/O
在介紹這部分內(nèi)容之前先來認(rèn)識下“異步I/O”。 說起異步IO,很容易聯(lián)想到同步I/O,對于同一個I/O對象句柄在同一時刻只允許一個I/O操作,其原理如下圖所示:
 顯然,當(dāng)內(nèi)核真正處理I/O的時間段(T2~T4),用戶線程是處于等待狀態(tài)的,如果這個時間段比較段的話,沒有什么影響;倘若這個時間段很長的話,線程就會長時間處于掛起狀態(tài)。事實(shí)上,該線程完全可以利用這段時間用處理其他事務(wù)。
異步I/O恰好可以解決同步I/O中的問題,而且支持對同一個I/O對象的并行處理,其原理如下圖所示: 
異步I/O在I/O請求完成時,可以使用讓I/O對象或者事件對象受信來通知用戶線程,而用戶線程中可以使用GetOverlappedResult來查看I/O的執(zhí)行情況。 由于異步I/O在進(jìn)行I/O請求后會立即返回,這樣就會產(chǎn)生一個問題:“程序是如何取得I/O處理的結(jié)果的?”。
有多種方法可以實(shí)現(xiàn)異步I/O,其不同資料上的分類一般都不盡相同,但原理上都類似,這里我把實(shí)現(xiàn)異步I/O的方法分為3類,本文就針對這3類方法進(jìn)行詳細(xì)的討論。 (1)重疊I/O (2)異步過程調(diào)用(APC),擴(kuò)展I/O (3)使用完成端口(IOCP)
二、使用重疊I/O實(shí)現(xiàn)異步I/O 同一個線程可以對多個I/O對象進(jìn)行I/O操作,不同的線程也可以對同一個I/O對象進(jìn)行操作,在我的理解中,重疊的命名就是這么來的。
在使用重疊I/O時,線程需要創(chuàng)建OVERLAPPED結(jié)構(gòu)以供I/O處理。該結(jié)構(gòu)中最重要的成員是hEvent,它是作為一個同步對象而存在,如果hEvent為NULL,那么此時的同步對象即為文件句柄、管道句柄等I/O操作對象。當(dāng)I/O完成后,會使這里的同步對象受信,從而通知用戶線程。
由于在進(jìn)行I/O請求后會立即返回,但有時用戶線程需要知道I/O當(dāng)前的執(zhí)行情況,此時就可以使用GetOverlappedResult。如果該函數(shù)的bWait參數(shù)為true,那么改函數(shù)就會阻塞線程直到目標(biāo)I/O處理完成為止;如果bWait為false,那么就會立即返回,如果此時的I/O尚未完,調(diào)用GetLastError就會返回ERROR_IO_INCOMPLETE。
代碼示例一: 代碼:
DWORD nReadByte ;
BYTE bBuf[BUF_SIZE] ;
OVERLAPPED ov = { 0, 0, 0, 0, NULL } ; // hEvent = NULL ;
HANDLE hFile = CreateFile ( ……, FILE_FLAG_OVERLAPPED, …… ) ;
ReadFile ( hFile, bBuf, sizeof(bBuf), &nReadByte, &ov ) ;
// 由于此時hEvent=NULL,所以同步對象為hFile,下面兩句的效果一樣
WaitForSingleObject ( hFile, INFINITE ) ;
//GetOverlappedResult ( hFile, &ov, &nRead, TRUE ) ;
這段代碼在調(diào)用ReadFile后會立即返回,但在隨后的WaitForSingleObject或者GetOverlappedResult中阻塞,利用同步對象hFile進(jìn)行同步。
這段代碼在這里可以實(shí)現(xiàn)正常的異步I/O,但存在一個問題,倘若現(xiàn)在需要對hFile句柄進(jìn)行多個I/O操作,就會出現(xiàn)問題。見下面這段代碼。
代碼示例二:
代碼:
DWORD nReadByte ;
BYTE bBuf1[BUF_SIZE],bBuf2[BUF_SIZE],bBuf3[BUF_SIZE] ;
OVERLAPPED ov1 = { 0, 0, 0, 0, NULL } ;
OVERLAPPED ov2 = { 0, 0, 0, 0, NULL } ;
OVERLAPPED ov3 = { 0, 0, 0, 0, NULL } ;
HANDLE hFile = CreateFile ( ……, FILE_FLAG_OVERLAPPED, …… ) ;
ReadFile ( hFile, bBuf1, sizeof(bBuf1), &nReadByte, &ov1 ) ;
ReadFile ( hFile, bBuf2, sizeof(bBuf2), &nReadByte, &ov2 ) ;
ReadFile ( hFile, bBuf3, sizeof(bBuf3), &nReadByte, &ov3 ) ;
//假設(shè)三個I/O處理的時間比較長,到這里還沒有結(jié)束
GetOverlappedResult ( hFile, &ov1, &nRead, TRUE ) ;
這里對于hFile有三個重疊的I/O操作,但他們的同步對象卻都為hFile。使用GetOverlappedResult進(jìn)行等待操作,這里看似在等待第一個I/O處理的完成,其實(shí)只要有任何一個I/O處理完成,該函數(shù)就會返回,相當(dāng)于忽略了其他兩個I/O操作的結(jié)果。
其實(shí),這里有一個很重要的原則:對于一個重疊句柄上有多于一個I/O操作的時候,應(yīng)該使用事件對象而不是文件句柄來實(shí)現(xiàn)同步。正確的實(shí)現(xiàn)見示例三。 代碼示例三:
代碼:
DWORD nReadByte ;
BYTE bBuf1[BUF_SIZE],bBuf2[BUF_SIZE],bBuf3[BUF_SIZE] ;
HANDLE hEvent1 = CreateEvent ( NULL, FALSE, FALSE, NULL ) ;
HANDLE hEvent2 = CreateEvent ( NULL, FALSE, FALSE, NULL ) ;
HANDLE hEvent3 = CreateEvent ( NULL, FALSE, FALSE, NULL ) ;
OVERLAPPED ov1 = { 0, 0, 0, 0, hEvent1 } ;
OVERLAPPED ov2 = { 0, 0, 0, 0, hEvent2 } ;
OVERLAPPED ov3 = { 0, 0, 0, 0, hEvent3 } ;
HANDLE hFile = CreateFile ( ……, FILE_FLAG_OVERLAPPED, …… ) ;
ReadFile ( hFile, bBuf1, sizeof(bBuf1), &nReadByte, &ov1 ) ;
ReadFile ( hFile, bBuf2, sizeof(bBuf2), &nReadByte, &ov2 ) ;
ReadFile ( hFile, bBuf3, sizeof(bBuf3), &nReadByte, &ov3 ) ;
//此時3個I/O操作的同步對象分別為hEvent1,hEvent2,hEvent3
GetOverlappedResult ( hFile, &ov1, &nRead, TRUE ) ;
這樣,這個GetOverlappedResult就可以實(shí)現(xiàn)對第一個I/O處理的等待 關(guān)于重疊I/O的就討論到這里,關(guān)于重疊I/O的實(shí)際應(yīng)用,可以參考《Windows系統(tǒng)編程之進(jìn)程通信》其中的命名管道實(shí)例。 http://bbs.pediy.com/showthread.php?s=&threadid=26252 三、 使用異步過程調(diào)用實(shí)現(xiàn)異步I/O
異步過程調(diào)用(APC),即在特定的上下文中異步的執(zhí)行一個調(diào)用。在異步I/O中可以使用APC,即讓操作系統(tǒng)的IO系統(tǒng)在完成異步I/O后立即調(diào)用你的程序。(在有些資料中,把異步I/O中的APC稱為“完成例程”,感覺這個名稱比較貼切,下文就以“完成例程”來表述。另外通常APC是作為線程同步這一塊的內(nèi)容,這里盡量淡化這個概念以免混淆。關(guān)于APC的詳細(xì)內(nèi)容到線程同步時再介紹 )
這里需要注意三點(diǎn): (1) APC總是在調(diào)用線程中被調(diào)用; (2) 當(dāng)執(zhí)行APC時,調(diào)用線程會進(jìn)入可變等待狀態(tài); (3) 線程需要使用擴(kuò)展I/O系列函數(shù),例如ReadFileEx,WriteFileEx, 另外可變等待函數(shù)也是必須的(至少下面其中之一): WaitForSingleObjectEx WaitForMultipleObjectEx SleepEx SignalObjectAndWait MsgWaitForMultipleObjectsEx 在使用ReadFileEx,WriteFileEx時,重疊結(jié)構(gòu)OVERLAPPED中的hEvent成員并非一定要指定,因?yàn)橄到y(tǒng)會忽略它。當(dāng)多個IO操作共用同一個完成例程時,可以使用hEvent來攜帶序號等信息,用于區(qū)別不同的I/O操作,因?yàn)樵撝丿B結(jié)構(gòu)會傳遞給完成例程。如果多個IO操作使用的完成例程都不相同時,則直接把hEvent設(shè)置為NULL就可以了。
在系統(tǒng)調(diào)用完成例程有兩個條件: (1) I/O操作必須完成 (2) 調(diào)用線程處于可變等待狀態(tài)
對于第一個條件比較容易,顯然完成例程只有在I/O操作完成時才調(diào)用;至于第二個條件就需要進(jìn)行認(rèn)為的控制,通過使用可變等待函數(shù),讓調(diào)用線程處于可變等待狀態(tài),這樣就可以執(zhí)行完成例程了。這里可以通過調(diào)節(jié)調(diào)用可變等待函數(shù)的時機(jī)來控制完成例程的執(zhí)行,即可以確保完成例程不會被過早的執(zhí)行。
當(dāng)線程具有多個完成例程時,就會形成一個隊列。使用可變等待函數(shù)使線程進(jìn)入可變等待狀態(tài)時有一個表示超時值的參數(shù),如果使用INFINITE,那么只有所有排隊的完成例程被執(zhí)行或者句柄獲得信號時該等待函數(shù)才返回。
上面已經(jīng)對利用完成例程實(shí)現(xiàn)異步I/O的一些比較重要的細(xì)節(jié)進(jìn)行的簡潔的闡述,接下來就以一個實(shí)例來說明完成例程的具體實(shí)現(xiàn)過程。
實(shí)例一:使用完成例程的異步I/O示例
1、 設(shè)計目標(biāo) 體會完成例程的異步I/O實(shí)現(xiàn)原理及過程。
2、 問題的分析與設(shè)計 設(shè)計流程圖如下:  示圖說明: 三個IO操作分別是IO_A, IO_B, IO_C, 他們的完成例程分別是APC_A, APC_B, APC_C。IO_A, IO_B是兩個很短的IO操作,IO_C是一個比較費(fèi)時的IO操作。 3、 詳細(xì)設(shè)計(關(guān)鍵代碼如下,具體參見附件中的源代碼CompletionRoutine)
代碼:
VOID WINAPI APC_A ( DWORD dwError, DWORD cbTransferred, LPOVERLAPPED lpo )
{
pTempInfo.push_back ( "執(zhí)行IO_A的完成例程" ) ;
}
VOID WINAPI APC_B ( DWORD dwError, DWORD cbTransferred, LPOVERLAPPED lpo )
{
pTempInfo.push_back ( "執(zhí)行IO_B的完成例程" ) ;
}
VOID WINAPI APC_C ( DWORD dwError, DWORD cbTransferred, LPOVERLAPPED lpo )
{
pTempInfo.push_back ( "執(zhí)行IO_C的完成例程" ) ;
}
void CCompletionRoutineDlg::OnTest()
{
// TODO: Add your control notification handler code here
HANDLE hFile_A, hFile_B, hFile_C ;
OVERLAPPED ov_A = {0}, ov_B = {0}, ov_C = {0} ;
#define C_SIZE 1024 * 1024 * 32
string szText_A = "Sample A !" ;
string szText_B = "Sampel B !" ;
string szText_C ;
szText_C.resize ( C_SIZE ) ;
memset ( &(szText_C[0]), 0x40, C_SIZE ) ;
pTempInfo.clear () ;
hFile_A = CreateFile ( "A.txt", GENERIC_WRITE, 0, NULL, \
CREATE_ALWAYS, FILE_FLAG_OVERLAPPED, NULL ) ;
hFile_B = CreateFile ( "B.txt", GENERIC_WRITE, 0, NULL, \
CREATE_ALWAYS, FILE_FLAG_OVERLAPPED, NULL ) ;
hFile_C = CreateFile ( "C.txt", GENERIC_WRITE, 0, NULL, \
CREATE_ALWAYS, FILE_FLAG_OVERLAPPED, NULL ) ;
WriteFileEx ( hFile_A, &(szText_A[0]), szText_A.length(), &ov_A, APC_A ) ;
pTempInfo.push_back ( "啟動IO_A, 并立即返回" ) ;
WriteFileEx ( hFile_B, &(szText_B[0]), szText_B.length(), &ov_B, APC_B ) ;
pTempInfo.push_back ( "啟動IO_B, 并立即返回" ) ;
WriteFileEx ( hFile_C, &(szText_C[0]), szText_C.size(), &ov_C, APC_C ) ;
pTempInfo.push_back ( "啟動IO_C, 并立即返回" ) ;
pTempInfo.push_back ( "進(jìn)入可變等待狀態(tài)" ) ;
SleepEx ( 1, true ) ;
pTempInfo.push_back ( "結(jié)束可變等待狀態(tài)" ) ;
pTempInfo.push_back ( "進(jìn)入可變等待狀態(tài)" ) ;
SleepEx ( 10000, true ) ;
pTempInfo.push_back ( "結(jié)束可變等待狀態(tài)" ) ;
CloseHandle ( hFile_A ) ;
CloseHandle ( hFile_B ) ;
CloseHandle ( hFile_C ) ;
m_ListBox.ResetContent () ;
list<string>::iterator p ;
for ( p = pTempInfo.begin(); p != pTempInfo.end(); p++ )
{
m_ListBox.AddString ( p->data() ) ;
}
DeleteFile ( "A.txt" ) ;
DeleteFile ( "B.txt" ) ;
DeleteFile ( "C.txt" ) ;
}
執(zhí)行后的效果如下(WinXP+SP2+VC6.0): 
4、 心得體會 每當(dāng)一個IO操作結(jié)束時會產(chǎn)生一個完成信息,如果該IO操作有完成例程的話就添加到完成例程隊列。一旦調(diào)用線程進(jìn)入可變等待狀態(tài),就會依次執(zhí)行隊列中的完成例程。 在這個示例中還有一個問題,如果把這個軟件放在系統(tǒng)分區(qū)的文件目錄下可以正常執(zhí)行,而放在其他盤符下就會出現(xiàn)問題,執(zhí)行結(jié)果就不同,真是奇怪了。
四、使用完成端口(IOCP)
實(shí)例二、使用IOCP的異步I/O示例 1、設(shè)計目標(biāo) 體會完成端口的異步I/O實(shí)現(xiàn)原理及過程。
2、 問題的分析與設(shè)計

說明: 每個客戶端與一個管道進(jìn)行交互,而在交互過程中I/O操作結(jié)束后產(chǎn)生的完成包就會進(jìn)入“I/O完成包隊列”。完成端口的線程隊列中的線程使用GetQueuedCompletionStatus來檢測“I/O完成包隊列”中是否有完成包信息。 3、詳細(xì)設(shè)計(關(guān)鍵代碼如下,具體見附件中的源碼)
代碼:
UINT ServerThread ( LPVOID lpParameter )
{
……
while ( true )
{
GetQueuedCompletionStatus ( pMyDlg->hCompletionPort, &cbTrans, &dwCompletionKey, &lpov, INFINITE ) ;
if ( dwCompletionKey == -1 )
break ;
// 讀取管道信息
// 響應(yīng)管道信息(寫入)
}
return 0 ;
}
void CMyDlg::OnStart()
{
// 創(chuàng)建完成端口
hCompletionPort = CreateIoCompletionPort ( INVALID_HANDLE_VALUE, NULL, 0, nMaxThread ) ;
CString lpPipeName = "\\\\.\\Pipe\\NamedPipe" ;
for ( UINT i = 0; i < nMaxPipe; i++ )
{
// 創(chuàng)建命名管道
PipeInst[i].hPipe = CreateNamedPipe ( lpPipeName, PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED, \
PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT, nMaxPipe, 0, 0, INFINITE, NULL ) ;
……
// 把命名管道與完成端口關(guān)聯(lián)起來
HANDLE hRet = CreateIoCompletionPort ( PipeInst[i].hPipe, hCompletionPort, i, nMaxThread ) ;
……
// 等待連接
ConnectNamedPipe ( PipeInst[i].hPipe, &(PipeInst[i].ov) ) ;
}
// 創(chuàng)建線程
for ( i = 0; i < nMaxThread; i++ )
{
hThread[i] = AfxBeginThread ( ServerThread, NULL, THREAD_PRIORITY_NORMAL ) ;
}
……
}
void CMyDlg::OnStop()
{
for ( UINT i = 0; i < nMaxThread; i++ )
{
// 用來喚醒線程的虛假I/O完成包
PostQueuedCompletionStatus ( hCompletionPort, 0, -1, NULL ) ;
CloseHandle ( hThread[i] ) ;
}
for ( i = 0; i < nMaxPipe; i++ )
{
DisconnectNamedPipe ( PipeInst[i].hPipe ) ;
CloseHandle ( PipeInst[i].hPipe ) ;
}
……
}
4、心得體會 上面這個例子是關(guān)于完成端口的簡單應(yīng)用。可以這樣來理解完成端口,它與三種資源相關(guān)分別是管道、I/O完成包隊列、線程隊列,它的作用是協(xié)調(diào)這三種資源。 【參考文獻(xiàn)】 [1]. Windows系統(tǒng)編程. Johnson M.Hart著 【版權(quán)聲明】必須注明來源看雪技術(shù)論壇(bbs.pediy.com) 及作者,并保持文章的完整性。
轉(zhuǎn)自: http://andylin02.iteye.com/blog/476399
|