- 在串行模式下,单个线程等待一个客户发出请求(通常是通过网络)。当来了请求后,线程醒来处理客户的请求。
- 在并发模型下,单个线程等待客户发出请求,而后创建新线程来处理请求。当新线程处理客户请求时,起初的线程循环回去等待另一个客户请求。处理客户请求的线程处理完毕后终结。
串行模型的问题在于它不能很好地处理好多个同时的请求,只适用于最简单的服务程序。Ping服务器是串行服务器的一个很好的例子。
因此并发模型就是最普通的了。它为每个请求都创建了一个新线程。而且通过增加硬件能力,会很容易使它的性能提高。
当并发模型实现在 NT 上时,微软 NT 小组注意到这些应用程序的性能没有预料得那么高。特别是有很多线程运行着的时候。因为所有这些线程都是可运行的(没有被挂起或等待什么事),微软意识到NT内核花了太多的时间来转换运行线程的上下文(context),而真正留给线程来做它们自己的工作的时间却被压缩了。
// 这个情况可以以我的一个例子来说明,我曾经花了一个下午去兵马俑,结果来去花在路上的时间有4个小时,而在兵马俑只呆了40分钟。这个例子有点夸张,不过夸张有助于理解 ;)
要使NT成为一个强大的服务器环境,微软就需要解决这个问题。解决的方法是一个称为I/O完成端口的内核对象,它首次在NT3.5中被引入。I/O完成端口的理论基础是并行运行的线程的数目必须有一个上限。500个同时的客户请求,并不意味着500个运行的线程。但并发运行的合适的线程数是多少呢?只要可运行的线程数多于CPU数,操作系统一定要花时间来进行线程上下文的切换的。
并行模型的一个低效之处是为每一个客户请求创建了一个新线程。创建线程比起创建进程来开销要小,但也远不是没有开销。如果当应用程序初始化时创建了一个线程池,而这些线程在应用程序执行期间是空闲的,程序的性能就能进一步提高。I/O完成端口就使用线程池。
I/O完成端口可能是Win32提供的最复杂的内核对象。要创建I/O完成端口,应调用 CreateIoCompletionPort:
HANDLE CreateIoCompletionPort(HANDLE hFileHandle, HANDLE hExistingCompletionPort, DWORD dwCompletionKey, DWORD dwNumberOfConcurrentThreads);
前三个参数只在把完成端口同设备相关联的时候才有用。如果不关联设备,只创建完成端口,那么前三个参数可以为:INVALID_HANDLE_VALUE,NULL,0。最后一个参数指示I/O完成端口同时能运行的最多线程数。如果为0,那么默认为机器上的CPU数。不过你可以用几个不同的值做实验来确定哪个值有最佳的性能。顺便说一句,这个函数是唯一一个创建了内核对象,而没有 LPSECURITY_ATTRIBUTES 参数的 Win32 函数。这是因为完成端口只应用于一个进程内。
当你创建一个I/O完成端口时,内核实际上创建了5个不同的数据结构。
第一个是设备列表。所有与完成端口相关联的设备都会出现在这个列表里,结构就是:
当调用 CreateIoCompletionPort 关联设备时,表项就增加;当设备句柄被关闭时,表项被删除。
设备可以是:一个文件,socket,邮件槽或管道等等。完成键可以自定义。
第二个数据结构是一个I/O完成队列。当一个设备的异步I/O请求完成时,系统检查该设备是否关联了一个完成端口。如果是,系统就向该完成端口的I/O完成队列里加入完成的I/O请求项。该队列中的每条表项给出了传输的字节数,32位完成键,I/O请求的OVERLAPPED结构的指针和一个错误码。
当I/O请求完成时或当PostQueuedCompletionStatus被调用时,表项被增加;当“等待线程队列”中删除一条表项时,表项被删除。
当服务应用程序初始化时,它应该创建I/O完成端口,而后应该创建一个线程池来处理客户请求。现在的问题在于池中应该有多少线程。这是一个很难回答的问题。一个标准的答案是将计算机上的CPU的数目乘以2。
池中的所有线程应该执行同一个线程函数。一般说来,该线程函数执行一些初始化后进入一个循环,该循环在服务进程终止时才结束。在循环中,线程使自己睡眠来等待完成端口的设备I/O请求的完成。这是通过 GetQueuedCompletionStatus 来实现的:
BOOL GetQueuedCompletionStatus(HANDLE hCompletionPort, LPDWORD lpdwNumberOfBytesTransferred, LPDWORD lpdwCompletionKey, LPOVERLAPPED* lpOverlapped, DWORD dwMilliseconds);
第一个参数指出线程要监视哪个完成端口。很多服务应用程序只使用一个I/O完成端口,所有的I/O请求完成通知都发给了该端口。简单地说,GetQueuedCompletionStatus 使调用线程进入睡眠,直到指定的完成端口的I/O完成队列中出现了一项或直到超时。
当线程池中的一个线程调用 GetQueuedCompletionStatus 时,调用线程的 ID 就被放入等待线程队列中。这样,I/O完成端口对象总是知道哪个线程正在等待处理完成的I/O请求。当完成队列里出现一项时,完成端口就唤醒等待线程队列里的一个线程,并把所有信息通过参数传过去。
要注意如何处理 GetQueuedCompletionStatus 的返回: