代码:
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 ) ;
}
……
}