系統編程之非同步I/O和完成連接埠
一、 同步I/O和非同步I/O
在介紹這部分內容之前先來認識下“非同步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 ) ;
}
……
}