CreateIoCompletionPort函数把一个或多个文件句柄关联到一个I/O完成端口。当对其中某个文件的异步I/O操作完成时,一个I/O完成包在相应的I/O完成端口排队。这可以把多个文件的同步点组合到一个对象中。
线程调用GetQueuedCompletionStatus等待一个完成包到达完成端口,而不是直接等待异步I/O操作完成。线程池中所有线程都阻塞在这个函数,当一个完成包到达完成端口时,线程按后入先出(LIFO)的顺序释放。这意味着当一个完成包到达完成端口时,系统释放最后一个在该函数阻塞的线程来处理完成请求。
一个线程调用GetQueuedCompletionStatus之后,它就被绑定到这个完成端口,直到它退出、或者绑定到不同的完成端口、或者调用CloseHandle撤销到该完成端口的绑定。一个线程最多只能绑定到一个完成端口。
完成端口最重要的特性就是并发量,其值是在完成端口创建时指定的。它限定了绑定到本完成端口的线程的可运行个数。当绑定到某完成端口的可运行的线程个数超过并发量,系统会阻塞后续线程的执行,直到可运行线程的个数低于并发量。当完成端口的队列中有完成包在排队,而可运行线程的个数也达到并发量,这时系统是最高效的,因为当一个运行着的线程调用GetQueuedCompletionStatus,它会立刻取得完成包,这样避免了操作系统内部的“线程上下文环境切换”。
一般情况下,并发量设为CPU的个数,但如果对一个完成包的处理是比较耗时的操作,应该设个较大值,具体多少最好由试验值来定。
PostQueuedCompletionStatus函数允许应用程序不通过发起一个异步I/O操作就可以发送一个自定义的完成包,这样就可以把外部事件通知线程池中的工作线程。
完成端口也是通过引用计数来维持它的生命周期,当没有外部引用时,完成端口被释放。所有与完成端口绑定的文件句柄都引用了它,所以在释放完成端口之前,必须释放所有绑定到它的文件句柄。
使用I/O完成端口
I/O完成端口是使用线程池的一种机制,除了负责处理异步I/O请求的工作者线程之外,还需要一个主线程创建、管理完成端口和线程池中的工作者线程。
主线程的执行过程如下:
- 调用CreateIoCompletionPort创建完成端口;
- 根据CPU个数创建一定数量的线程;
- 调用CreateIoCompletionPort把异步I/O操作涉及的文件句柄和第1步创建的完成端口关联。
工作者线程的执行过程如下:
- 调用GetQueuedCompletionStatus等待异步I/O完成包到达;
- 根据完成包的的内容做响应处理。
- 循环执行1~2。
整个机制中,最关键的是I/O完成包的流向。
当调用一个异步操作接口(如调用WSARecv、WSASend或者调用ReadFileEx、WriteFileEx去读一个以FILE_FLAG_OVERLAPPED标志打开的文件)时,应用程序就向系统内核发起了一次异步I/O请求。这时需要传递一个LPOVERLAPPED结构和一个LPOVERLAPPED_COMPLETION_ROUTINE回调函数指针(指向异步I/O完成时回调的函数)给异步操作接口作参数。
当系统内核处理完某异步I/O请求后,如果异步I/O请求发起时传入的LPOVERLAPPED_COMPLETION_ROUTINE不为空,该函数会被调用。对于I/O完成端口模型,该值都为空,这样系统内核构造一个I/O完成包把它放入I/O完成端口的队列。
线程池中的工作者线程不断调用GetQueuedCompletionStatus从I/O完成端口的队列中取出I/O完成包,根据完成包的的内容做响应处理。
由此可见,I/O完成端口是个抽象的实体,它拥有(对应、管理)一个线程池(其中包括若干工作者线程)和一个I/O完成包的队列。
同样,I/O完成包也是个抽象实体,它至少对应一个异步I/O操作的目标(可以是文件和Socket)句柄和一个描述一次异步I/O操作的结构,这样工作者线程就有了作出响应的依据。
GetQueuedCompletionStatus的三个输出参数都可以看成是I/O完成包的内容,其中
BOOL GetQueuedCompletionStatus( [in]HANDLE CompletionPort, [out]LPDWORD lpNumberOfBytes, [out]PULONG_PTR lpCompletionKey, [out]LPOVERLAPPED* lpOverlapped, [in]DWORD dwMilliseconds ); |
lpNumberOfBytes 返回本次异步I/O操作完成传输的字节数,该值由系统内核填充;
lpCompletionKey 返回本次异步I/O操作目标的相关信息,该值返回的仅仅是个内存地址,具体是些什么数据如何分布,由把异步I/O操作目标句柄和完成端口关联时(即主线程在第3步)传给CreateIoCompletionPort的第三个参数指定,即CreateIoCompletionPort第三个参数指向的东西就是lpCompletionKey指向的东西,系统不作任何改动。要注意的是,CreateIoCompletionPort在把多个文件句柄管关联到同一个完成端口时,为每个句柄指定不同的CompletionKey值,内核在完成一次异步I/O操作时,取出相应的CompletionKey值放入I/O完成包。因而在Anthony Jones, Jim Ohlund写的《Network Programming for Microsoft Windows》中,这部分数据被称为Per-handle Data,也就是说这部分数据和完成端口关联的诸多文件句柄是一一对应的。
HANDLE CreateIoCompletionPort( [in]HANDLE FileHandle, [in]HANDLE ExistingCompletionPort, [in]ULONG_PTR CompletionKey, [in]DWORD NumberOfConcurrentThreads ); |
lpOverlapped 表面上它返回的就是一个LPOVERLAPPED,但在实际应用中通常通过它来传递描述一次异步I/O操作的相关信息,所以该值返回的也仅仅是个内存地址,具体是些什么数据如何分布,与发起异步I/O请求时传给异步API的LPOVERLAPPED/LPWSAOVERLAPPED 参数有关。在Anthony Jones, Jim Ohlund写的《Network Programming for Microsoft Windows》中,这部分数据被称为Per-I/O Operation Data,因为这部分数据和一次异步I/O操作一一对应。和lpCompletionKey不同的是,系统要求传入的lpOverlapped所指向的必须是个LPOVERLAPPED/LPWSAOVERLAPPED 结构,并且也会对这部分进行修改,但对lpOverlapped+sizeof(LPOVERLAPPED)之外的内存不作任何修改。
typedef struct _PER_IO_DATA LPPER_IO_DATA lpPerIoData = (LPPER_IO_DATA)GlobalAlloc(GPTR, sizeof(PER_IO_DATA)); //这里把Overlapped作为PER_IO_DATA的第一个成员,所以直接转换即可,否则 WSARecv(?,?,?,?,?,&(lpPerIoData->Overlapped),NULL); GetQueuedCompletionStatus取得I/O完成包后,如果Overlapped是PER_IO_DATA的第一个成员,lpOverlapped所指的也就是lpPerIoData的地址,工作者线程可以获取OperationType成员的值;如果Overlapped不是PER_IO_DATA的第一个成员,可以使用CONTAINING_RECORD宏来获取lpPerIoData的地址: lpPerIoData = (LPPER_IO_DATA)CONTAINING_RECORD(lpOverlapped, PER_IO_DATA, Overlapped); |
from http://hzgmaxwell.bokee.com/4564081.html