围绕I/O完成端口的架构

在服务应用程序初始化时,将会通过CreateNewCompletionPort之类的函数创建I/O完成端口。应用程序也需要创建一个线程池来处理客户端请求。现在要问的问题是:线程池内应该有多少线程?这个问题较难回答,所以将细节放到小节“线程池内有多少线程”中。迄今为止,一个标准的规则是CPU数量乘以2。因此,对于双CPU的机器,应该创建包含4个线程的线程池。

线程池内的所有线程应该执行同一个函数。典型地,该线程函数进行初始化工作,然后进行循环,直到服务进程收到停止的指令。在循环内,线程进入休眠状态,等待完成端口的设备I/O请求完成。调用GetQueeudCompletionStatus可以达到这个目的:

BOOL GetQueuedCompletionStatus(
   HANDLE       hCompPort,
   PDWORD       pdwNumBytes,
   PULONG_PTR   CompKey,
   OVERLAPPED** ppOverlapped,
   DWORD        dwMilliseconds);

第一个参数,hCompPort,表示线程所关注的完成端口。许多服务应用程序使用单个I/O完成端口,并将所有的I/O请求通知完成到该端口。基本上,GetQueuedCompletionStatus的工作就是使线程进入休眠状态,直到指定的完成端口的I/O完成端口出现一个条目,或者指定的超时时间达到(由dwMilliseconds参数指定)。

第三个与I/O完成端口关联的数据结构是正在线程等待队列。线程池内每个调用GetQueuedCompletionStatus的线程的ID被放到正在线程等待队列中,以使I/O完成端口内核对象能够知道当前哪些线程正在等待处理完成的I/O请求。当在该完成端口的I/O完成队列中出现新的项时,完成端口从正在线程等待队列中挑出一个线程唤醒。被唤醒的线程将得到以下信息来组织一个已完成的I/O项:传输的字节数,完成键值,OVERLAPPED结构的地址。这些信息经由pdwNumBytes, pCompKey, ppOverlapped参数返回。

检查GetQueueCompletionStatus的返回原因有点麻烦;下面的代码演示了正确的方法:

DWORD dwNumBytes;
ULONG_PTR CompKey;
OVERLAPPED* pOverlapped;

// hIOCP 在程序的其他地方被初始化
BOOL fOk = GetQueuedCompletionStatus(hIOCP, &dwNumBytes, &CompKey, &pOverlapped, 1000);
DWORD dwError = GetLastError();

if (fOk) {
   // 成功处理了一个完成的I/O请求
} else {
   if (pOverlapped != NULL) {
      // 处理完成的I/O请求失败
      // dwError 包含了失败的原因
   } else {
      if (dwError == WAIT_TIMEOUT) {
         // 等待完成I/O项超时
      } else {
         // GetQueuedCompletionStatus的错误调用
         // dwError 指出了错误调用的原因
      }
   }
}

正如所期望的那样,I/O完成队列中的项是以先进先出(FIFO)的方式删除的。但是,出乎意料的是,调用GetQueuedCompletionStatus的线程却是以后进先出(LIFO)的方式被唤醒。这么做的原因是为了提高性能。比如说,在线程等待队列中有四个线程,当已完成的I/O项出现时,最后一个调用GetQueuedCompletionStatus的线程将被唤醒来处理该项。这个最后的线程在处理完成后,又调用GetQueuedCompletionStatus重新进入线程等待队列。现在如果又出现了一个I/O完成项,同一线程又会被唤醒来处理新项。

当I/O请求的完成慢到单个线程都能够处理时,系统将一直唤醒同一线程进行处理,其他三个线程持续休眠。通过使用LIFO算法,没有被调度的线程可以将它们的内存资源(如堆栈空间)对换到磁盘并从进程的缓冲区内清空。这意味着即使众多线程在完成端口上等待也并非坏事。如果有几个线程在等待,但只有很少的I/O请求完成,多余的线程一定会将它们的大部分资源对换出系统。


已注销
6 声望2 粉丝

A babysitter.