Windows系统编程之异步I/O和完成端口 [北极星2003 看雪论坛]
在介紹這部分內容之前先來認識下“異步I/O”。
??說起異步IO,很容易聯想到同步I/O,對于同一個I/O對象句柄在同一時刻只允許一個I/O操作,其原理如下圖所示:
??
??顯然,當內核真正處理I/O的時間段(T2~T4),用戶線程是處于等待狀態的,如果這個時間段比較段的話,沒有什么影響;倘若這個時間段很長的話,線程就會長時間處于掛起狀態。事實上,該線程完全可以利用這段時間用處理其他事務。
??異步I/O恰好可以解決同步I/O中的問題,而且支持對同一個I/O對象的并行處理,其原理如下圖所示:
??
??異步I/O在I/O請求完成時,可以使用讓I/O對象或者事件對象受信來通知用戶線程,而用戶線程中可以使用GetOverlappedResult來查看I/O的執行情況。
??
由于異步I/O在進行I/O請求后會立即返回,這樣就會產生一個問題:“程序是如何取得I/O處理的結果的?”。
??有多種方法可以實現異步I/O,其不同資料上的分類一般都不盡相同,但原理上都類似,這里我把實現異步I/O的方法分為3類,本文就針對這3類方法進行詳細的討論。
(1)重疊I/O
(2)異步過程調用(APC),擴展I/O
(3)使用完成端口(IOCP)
二、使用重疊I/O實現異步I/O
??
??同一個線程可以對多個I/O對象進行I/O操作,不同的線程也可以對同一個I/O對象進行操作,在我的理解中,重疊的命名就是這么來的。
??在使用重疊I/O時,線程需要創建OVERLAPPED結構以供I/O處理。該結構中最重要的成員是hEvent,它是作為一個同步對象而存在,如果 hEvent為NULL,那么此時的同步對象即為文件句柄、管道句柄等I/O操作對象。當I/O完成后,會使這里的同步對象受信,從而通知用戶線程。
??由于在進行I/O請求后會立即返回,但有時用戶線程需要知道I/O當前的執行情況,此時就可以使用GetOverlappedResult。如果該函 數的bWait參數為true,那么改函數就會阻塞線程直到目標I/O處理完成為止;如果bWait為false,那么就會立即返回,如果此時的I/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?)?;
這段代碼在調用ReadFile后會立即返回,但在隨后的WaitForSingleObject或者GetOverlappedResult中阻塞,利用同步對象hFile進行同步。
??這段代碼在這里可以實現正常的異步I/O,但存在一個問題,倘若現在需要對hFile句柄進行多個I/O操作,就會出現問題。見下面這段代碼。
代碼示例二:
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?)?;
//假設三個I/O處理的時間比較長,到這里還沒有結束
GetOverlappedResult?(?hFile,?&ov1,?&nRead,?TRUE?)?;
??這里對于hFile有三個重疊的I/O操作,但他們的同步對象卻都為hFile。使用GetOverlappedResult進行等待操作,這里看似 在等待第一個I/O處理的完成,其實只要有任何一個I/O處理完成,該函數就會返回,相當于忽略了其他兩個I/O操作的結果。
??其實,這里有一個很重要的原則:對于一個重疊句柄上有多于一個I/O操作的時候,應該使用事件對象而不是文件句柄來實現同步。正確的實現見示例三。
??
代碼示例三:
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就可以實現對第一個I/O處理的等待
關于重疊I/O的就討論到這里,關于重疊I/O的實際應用,可以參考《Windows系統編程之進程通信》其中的命名管道實例。
http://bbs.pediy.com/showthread.php?s=&threadid=26252
?
三、??使用異步過程調用實現異步I/O
異步過程調用(APC),即在特定的上下文中異步的執行一個調用。在異步I/O中可以使用APC,即讓操作系統的IO系統在完成異步I/O后立即調用你的 程序。(在有些資料中,把異步I/O中的APC稱為“完成例程”,感覺這個名稱比較貼切,下文就以“完成例程”來表述。另外通常APC是作為線程同步這一 塊的內容,這里盡量淡化這個概念以免混淆。關于APC的詳細內容到線程同步時再介紹?)
這里需要注意三點:
(1)??APC總是在調用線程中被調用;
(2)??當執行APC時,調用線程會進入可變等待狀態;
(3)??線程需要使用擴展I/O系列函數,例如ReadFileEx,WriteFileEx,?另外可變等待函數也是必須的(至少下面其中之一):
WaitForSingleObjectEx
WaitForMultipleObjectEx
SleepEx
SignalObjectAndWait
MsgWaitForMultipleObjectsEx
??
??在使用ReadFileEx,WriteFileEx時,重疊結構OVERLAPPED中的hEvent成員并非一定要指定,因為系統會忽略它。當多 個IO操作共用同一個完成例程時,可以使用hEvent來攜帶序號等信息,用于區別不同的I/O操作,因為該重疊結構會傳遞給完成例程。如果多個IO操作 使用的完成例程都不相同時,則直接把hEvent設置為NULL就可以了。
在系統調用完成例程有兩個條件:
(1)??I/O操作必須完成
(2)??調用線程處于可變等待狀態
對于第一個條件比較容易,顯然完成例程只有在I/O操作完成時才調用;至于第二個條件就需要進行認為的控制,通過使用可變等待函數,讓調用線程處于可變等 待狀態,這樣就可以執行完成例程了。這里可以通過調節調用可變等待函數的時機來控制完成例程的執行,即可以確保完成例程不會被過早的執行。
當線程具有多個完成例程時,就會形成一個隊列。使用可變等待函數使線程進入可變等待狀態時有一個表示超時值的參數,如果使用INFINITE,那么只有所有排隊的完成例程被執行或者句柄獲得信號時該等待函數才返回。
上面已經對利用完成例程實現異步I/O的一些比較重要的細節進行的簡潔的闡述,接下來就以一個實例來說明完成例程的具體實現過程。
實例一:使用完成例程的異步I/O示例
1、??設計目標
體會完成例程的異步I/O實現原理及過程。
2、??問題的分析與設計
設計流程圖如下:
??
示圖說明:
??三個IO操作分別是IO_A,?IO_B,?IO_C,?他們的完成例程分別是APC_A,?APC_B,?APC_C。IO_A,?IO_B是兩個很短的IO操作,IO_C是一個比較費時的IO操作。
3、??詳細設計(關鍵代碼如下,具體參見附件中的源代碼CompletionRoutine)
VOID?WINAPI?APC_A?(?DWORD?dwError,?DWORD?cbTransferred,?LPOVERLAPPED?lpo?)
{
pTempInfo.push_back?(?"執行IO_A的完成例程"?)?;
}
VOID?WINAPI?APC_B?(?DWORD?dwError,?DWORD?cbTransferred,?LPOVERLAPPED?lpo?)
{
pTempInfo.push_back?(?"執行IO_B的完成例程"?)?;
}
VOID?WINAPI?APC_C?(?DWORD?dwError,?DWORD?cbTransferred,?LPOVERLAPPED?lpo?)
{
pTempInfo.push_back?(?"執行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?(?"進入可變等待狀態"?)?;
SleepEx?(?1,?true?)?;
pTempInfo.push_back?(?"結束可變等待狀態"?)?;
pTempInfo.push_back?(?"進入可變等待狀態"?)?;
SleepEx?(?10000,?true?)?;
pTempInfo.push_back?(?"結束可變等待狀態"?)?;
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"?)?;
}
執行后的效果如下(WinXP+SP2+VC6.0):
??
4、??心得體會
每當一個IO操作結束時會產生一個完成信息,如果該IO操作有完成例程的話就添加到完成例程隊列。一旦調用線程進入可變等待狀態,就會依次執行隊列中的完成例程。
在這個示例中還有一個問題,如果把這個軟件放在系統分區的文件目錄下可以正常執行,而放在其他盤符下就會出現問題,執行結果就不同,真是奇怪了。
四、使用完成端口(IOCP)
實例二、使用IOCP的異步I/O示例
1、設計目標
體會完成端口的異步I/O實現原理及過程。
2、??問題的分析與設計
?
說明:
??每個客戶端與一個管道進行交互,而在交互過程中I/O操作結束后產生的完成包就會進入“I/O完成包隊列”。完成端口的線程隊列中的線程使用GetQueuedCompletionStatus來檢測“I/O完成包隊列”中是否有完成包信息。?
3、詳細設計(關鍵代碼如下,具體見附件中的源碼)
UINT?ServerThread?(?LPVOID?lpParameter?)
{
……
while?(?true?)
{
GetQueuedCompletionStatus?(?pMyDlg->hCompletionPort,?&cbTrans,?&dwCompletionKey,?&lpov,?INFINITE?)?;
if?(?dwCompletionKey?==?-1?)
break?;
//?讀取管道信息
//?響應管道信息(寫入)
}
return?0?;
}
void?CMyDlg::OnStart()?
{
//?創建完成端口
hCompletionPort?=?CreateIoCompletionPort?(?INVALID_HANDLE_VALUE,?NULL,?0,?nMaxThread?)?;
CString?lpPipeName?=?""""".""Pipe""NamedPipe"?;
for?(?UINT?i?=?0;?i?<?nMaxPipe;?i++?)
{
//?創建命名管道
PipeInst[i].hPipe?=??CreateNamedPipe?(?lpPipeName,?PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED,?"
PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT,?nMaxPipe,?0,?0,?INFINITE,?NULL?)?;
……
//?把命名管道與完成端口關聯起來
HANDLE?hRet?=?CreateIoCompletionPort?(?PipeInst[i].hPipe,?hCompletionPort,?i,?nMaxThread?)?;
……
//?等待連接
ConnectNamedPipe?(?PipeInst[i].hPipe,?&(PipeInst[i].ov)?)?;
}
//?創建線程
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、心得體會
??上面這個例子是關于完成端口的簡單應用。可以這樣來理解完成端口,它與三種資源相關分別是管道、I/O完成包隊列、線程隊列,它的作用是協調這三種資源。
【參考文獻】
[1].?Windows系統編程.?Johnson?M.Hart著
轉載于:https://www.cnblogs.com/NeuqUstcIim/archive/2008/08/18/1270065.html
總結
以上是生活随笔為你收集整理的Windows系统编程之异步I/O和完成端口 [北极星2003 看雪论坛]的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 服务器在新加坡延迟,从国内访问新加坡服务
- 下一篇: Working Practice-召集相