本文仅是对Windows 在线文档(部分)的翻译
I/O完成端口为多处理器系统上的异步I/O请求提供了一个高效的线程模式。当进程创建一个I/O完成端口时,系统会创建相关的一系列队列。结合一个预先初始化的线程池,进程通过使用完成端口可以更快,更高效地处理大量并发的异步I/O请求。
1 I/O完成端口工作流程
CreateIoCompletionPort
函数用于创建I/O完成端口对象并将一个或多个文件句柄关联到该端口上。当这些文件句柄上的异步I/O操作完成时,会以先入先出的顺序向该完成端口的I/O完成队列压入一个完成封包。这个机制的强大之处在于将多个文件句柄的同步点整合到一个对象上。当然,这个机制也有其它用武之地。注意,虽然完成封包以先入先出的顺序入队,但可能以其它顺序出队。
术语file handle指一个抽象的重叠I/O设备,而不仅仅是磁盘上的文件。例如,可能是一个网络设备,TCP套接字,命名管道,或邮件槽。其可以是任何支持重叠I/O的系统对象。
当某文件句柄和完成端口关联后,除非该文件句柄上的完成封包从完成端口移除或原始操作同步地返回了错误,否则文件句柄的状态不会更新。线程(由主线程创建的其它线程或主线程自己)使用GetQueuedCompletionStatus
函数等待压入到完成端口队列的完成封包,而不是直接等待异步I/O完成。在完成端口上阻塞的线程将以后入先出的顺序唤醒,而下一个完成封包将会以先入先出的顺序从完成端口的I/O完成队列中拉取。也就是说,当将完成封包分配给线程处理时,系统会唤醒最近与该完成端口关联的线程。
在指定的完成端口上不限定调用GetQueuedCompletionStatus
的线程数。当线程第一次调用GetQueuedCompletionStatus
时,他将和指定的完成端口关联(等待线程队列),一直到以下情况发生:
- 线程退出
- 后续调用
GetQueuedCompletionStatus
时指定另外的完成端口 - 关闭了完成端口
换句话说,一个线程在同一时刻最多只能关联一个完成端口。
当有完成封包到达时,系统首先检查当前有多少与完成端口关联的线程正在运行。如果正在运行的线程数小于指定的最大并发数,会唤醒其中一个线程(最近的那个)以处理完成封包。当运行的线程完成其处理时,通常会再次调用GetQueuedCompletionStatus
,这时候,如果完成端口的I/O完成队列不为空,它将继续处理下一个完成封包,否则线程阻塞,等待完成封包。
线程可以调用PostQueuedCompltionStatus
函数向完成端口的I/O完成队列添加特殊的完成封包。这样完成端口还可被用来从进程的其它线程接收自定义消息。这通常用于告知工作线程某些外部事件,如应用程序即将终止运行。
I/O完成端口句柄以及与之关联的文件句柄一起称之为对完成端口的引用。只有当没有引用时,完成端口对象才能被释放。因此,为了释放完成端口对象及相关资源,所有这些句柄必须被正确地关闭。在所有这些条件满足后,应用程序必须调用CloseHandle
关闭完成端口句柄。
I/O完成端口和创建它的进程关联,无法在进程间共享,但是可以被进程内的线程共享。
2 线程和并发
完成端口最重要的属性是最大并发数。完成端口的最大并发数在其通过CreateIoCompletionPort
创建时由NumberOfConcurrentThreads参数指定。这个参数限制与完成端口关联的线程的可运行数。如果同完成端口关联的正在运行的线程数已达到指定的最大并发值,系统将阻止其它关联线程被唤醒,直到正在运行的线程数小于最大并发数。
最高效的情形是,当队列中有完成封包等待时,由于完成端口上正在运行的线程数已达到其最大并发数而不会唤醒任何其它线程。在这种情况下,如果完成端口的队列中总是有正在等待的完成封包,当正在运行的线程处理完上一个封包,然后调用GetQueuedCompletionStatus
时,其不会阻塞而是立即获得下一个完成封包并处理。这时没有线程上下文切换,因为运行中的线程是连续地获得完成封包的,同时其它线程仍然不能运行。
在上面的例子中,额外的线程似乎没什么用,因为它们从来不运行。但是上面的情况是假设运行线程从来不会因为其它机制而进入等待状态。
显然,合适的最大并发数是机器的CPU数。如果线程处理的事务需要长时间运算,更大的并发数将允许更多线程得以运行。有些完成封包可能需要较长的时间进行处理,但多数完成封包的处理时间是差不多的。可以通过数值试验获取最佳的最大并发数。
如果与完成端口关联的正在运行的线程因为其它原因进入等待状态,例如,调用了SuspendThread
函数,系统会允许因调用GetQueuedCompletionStatus
而等待运行的线程处理完成封包。当之前进入等待的线程又开始运行时,可能有一个短暂的时间实际运行的线程数大于最大并发数。但是系统会通过禁止唤醒其它等待线程而快速减小这个实际并发数。这就是为什么应用程序要将线程池线程数设置的比完成端口最大并发数大的原因。
3 支持函数
下面的函数可用于开始使用I/O完成端口的I/O操作。必须向函数传递OVERLAPPED
结构体实例且在此之前必须将相关的文件句柄和完成端口关联:
- ConnectNamedPipe
- DeviceIoControl
- LockFileEx
- ReadDirectoryChangesW
- ReadFile
- TransactNamedPipe
- WaitCommEvent
- WriteFile
- WSASendMsg
- WSASendTo
- WSARecvFrom
- WSARecvMsg
- WSARecv
4 APIs
CreateIoCompletionPort
创建一个I/O完成端口并将其和指定的文件句柄关联,或仅仅是创建一个完成端口。
在I/O完成端口上关联一个打开的文件句柄将允许进程接收该文件句柄上的异步I/O操作的完成通知。
这里文件句柄是一个系统抽象的名词,其代表一个重叠I/O端而不是磁盘上的一个文件。任何支持重叠I/O的系统对象,如网络端点、TCP socket、命名管道或mail slots都可当做文件句柄。
Syntax
HANDLE WINAPI CreateIoCompletionPort(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE ExistingCompletionPort,
_In_ ULONG_PTR CompletionKey,
_In_ DWORD NumberOfConcurrentThreads
);
Parameters
FileHandle
一个打开的文件句柄或者是INVALID_HANDLE_VALUE
。
该句柄必须是支持重叠I/O的对象。
如果指定了文件句柄,其必须以重叠I/O模式打开。例如,必须以FILE_FLAG_OVERLAPPED
标识调用CreateFile
函数以获得一个文件句柄。
如果指定了INVALID_HANDLE_VALUE
,函数将只是创建一个新的完成端口,这种情况下,ExistingCompletionPort必须是NULL
且CompletionKey会被忽略。
ExistingCompletionPort
一个已存在的I/O完成端口句柄或者是NULL。
如果指定了一个已存在的完成端口,函数会将其和参数FileHandle指定的文件句柄关联。如果函数执行成功返回该完成端口。
如果该参数为NULL
,函数将创建一个新的I/O完成端口。如果指定了有效的文件句柄(FileHandle),则新创建的完成端口会和其关联,否则只是新建一个完成端口。函数返回该新建的完成端口。
CompletionKey
包含于每个I/O完成封包中的用户自定义的pre-handle。
NumberOfConcurrentThreads
对每个I/O完成端口,操作系统允许的最大线程数以同时处理I/O完成封包。如果ExistingCompletionPort参数不为NULL,该参数会被忽略。
如果该参数为0,系统将允许和系统处理器个数一样的线程数同时运行。
Return value
函数执行成功必然返回一个I/O完成端口。
- 如果ExistingCompletionPort为
NULL
,返回一个新的完成端口。 - 如果ExistingCompletionPort为一个有效的完成端口,则返回这个完成端口。
- 如果FileHandle是一个有效的文件句柄,该文件句柄将会和返回的完成端口关联。
- 如果函数失败,返回
NULL
,可通过GetLastError
函数获取扩展错误码。
Remarks
I/O完成端口和创建它的进程关联,其它进程不可见,但是同一进程内的线程之间可共享。
文件句柄仅可和一个完成端口关联,一旦完成关联,直到该文件句柄关闭该关联将一直维持。
可多次调用CreateIoCompletionPort
函数将多个文件句柄关联到一个完成端口上。
使用CompletionKey参数帮助应用程序跟踪究竟是哪个重叠IO已经完成。这个参数并没有参与CreateIoCompletionPort
的内部功能控制,其只是绑定到文件句柄上。对于每个文件句柄来说这个CompletionKey必须是唯一的,且其在整个内部处理期间都会伴随文件句柄。当完成封包到来时,可通过GetQueuedCompletionStatus
函数获取这个CompletionKey。CompletionKey也可用于PostQueuedCompletionStatus
函数以入队用户自定义完成封包。
当文件句柄和某IO完成端口关联后,其不可再用于ReadFileEx
函数和WriteFileEx
函数,因为这些函数有它们自己的异步IO机制。
最好不要以句柄继承或调用DuplicateHandle
函数的方式共享已经与IO完成端口关联的文件句柄。使用这种多重句柄执行操作时也会产生完成通知,还是小心为妙。
IO完成端口句柄和与其关联的文件句柄我们称之为IO完成端口引用(reference to the I/O completion port),当没有引用时必须释放IO完成端口。所有这些句柄必须正确地关闭以释放IO完成端口及其关联的系统资源。当满足上述条件时,可调用CloseHandle
关闭IO完成端口。
GetQueuedCompletionStatus
尝试从指定的IO完成端口上出队一个IO完成封包。如果队列中没有完成封包,函数将等待完成端口上某个未决IO操作完成。
如果需要一次出队多个IO完成封包,使用GetQueuedCompletionStatusEx
函数。
Syntax
BOOL WINAPI GetQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_Out_ LPDWORD lpNumberOfBytes,
_Out_ PULONG_PTR lpCompletionKey,
_Out_ LPOVERLAPPED *lpOverlapped,
_In_ DWORD dwMilliseconds
);
Parameters
CompletionPort
lpNumberOfBytes
用于保存已完成的IO操作在其执行期间传输的字节总数。
lpCompletonKey
当某文件句柄上的IO操作完成时,用于保存与该文件句柄关联的completion key的值。
lpOverlapped
用于保存某OVERLAPPED结构体的地址,其在IO操作开始时被指定。
即使已将文件句柄和完成端口关联且指定了有效的OVERLAPPED
结构,应用程序也可以阻止完成通知。为此,必须在OVERLAPPED
结构的hEvent成员中保存一个有效的事件句柄并设置其最低位:
Overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
Overlapped.hEvent = (HANDLE) ((DWORD_PTR) Overlapped.hEvent | 1);
...
另外,在关闭这个事件句柄时不要忘了将最低位清掉:
CloseHandle((HANDLE) ((DWORD_PTR) Overlapped.hEvent & ~1));
dwMilliseconds
调用者愿意在完成端口上等待完成封包的毫秒数。如果在指定的时间内没有完成封包出现,函数将返回FALSE
同时设置lpOverlapped为NULL
。
如果为INFINITE
,函数将阻塞执行。
Return value
这个函数将线程和指定的完成端口关联。一个线程只能和一个完成端口关联。
当调用GetQueuedCompletionStatus
时如果与之关联的完成端口已经关闭,函数将失败并返回FALSE
,且lpOverlapped为NULL
,同时GetLastError
返回ERROR_ABANDONED_WAIT_0
扩展错误码。
如果GetQueuedCompletionStatus
函数执行成功,将从完成端口上出队一个完成封包(对应一个成功的IO操作),并将其信息存储到lpNumberOfBytes, lpCompletionKey和lpOverlapped参数中;如果执行失败,这些参数可能包含以下特定的值:
- 如果lpOverlapped为
NULL
,函数没有从完成端口上出队一个完成封包,这种情况下,函数不会在lpNumberOfBytes, lpCompletionKey中存储信息,它们的值是不确定的。 - 如果lpOverlapped不为
NULL
,函数从完成端口上出队一个完成封包,但该完成封包对应一个失败的IO操作,函数会将该失败的IO操作的信息存储到lpNumberOfBytes, lpCompletionKey和lpOverlapped中,可通过GetLastError
获取扩展错误码。
PostQueuedCompletionStatus
向完成端口提交一个IO完成封包。
Syntax
BOOL WINAPI PostQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_In_ DWORD dwNubmerOfBytesTransferred,
_In_ ULONG_PTR dwCompletionKey,
_In_opt_ LPOVERLAPPED lpOverlapped
);
Parameters
CompletionPort
dwNumberOfBytesTransferred
dwCompletonKey
lpOverlapped
上述3个参数指定当调用GetQueuedCompletionStatus
时对应参数带回的值。
Return value
函数执行成功返回非零值,否则返回0.
Remarks
提交的完成封包完全满足GetQueuedCompletonStatus
的要求。系统不会使用这个封包也不会验证其正确性。尤其是,lpOverlapped参数不必是指向OVERLAPPED
结构体的指针。