在这个言必称支持百万级用户的时代,似乎只要掌握IOCP就拿到了Windows平台下开发高性能服务端程序的入场券。网上的相关文章很多,写得都很经典。这里,我只想谈谈我的理解。
我是很早以前在研究Microsoft Soap Toolkit 1.0源码的时候第一次见识IOCP的,里面是把它当做一个任务分发队列来使用的。创建一个IO Complete Port,创建多个工作线程,每个线程试图从Port上读取数据(即GetQueuedCompletionStatus,取到后才返回)。当有一个任务要执行的时候,就发布(PostQueuedCompletionStatus)到IO Complete Port上,这时候就会有一个线程被唤醒(即GetQueuedCompletionStatus函数返回)执行该任务……你看,这并没有什么特别的地方,使用别的技术(比如Event)也可以做到。
当然,把IOCP拿来这样用是大材小用了。不过,本质上它就是一个消息队列。上面,我们是主动发布消息,实际上我们可以让OS来完成。这其实是有关异步IO的处理问题,下面解释。
之前我写文讨论过,Socket就是类似文件句柄一样的,归属IO操作的范畴。而IO操作的一个最大问题就是延迟,从硬盘读数据有延迟,从网络读数据也有延迟,延迟就是CPU需要等待,浪费时间了。
对付延迟,一般有两种方式。
第一种就是开多个线程。如果一个线程因为IO延迟处于等待状态的话,其它的线程可以继续使用CPU运行别的任务。线程对系统是有消耗的,特别是CPU在线程之间进行切换的时候。所以,线程数量不能太多,一般而言应该是等于CPU的个数。
第二种就是异步调用。一般我们使用的是同步调用,对于IO就是等待数据读取或写入完成后调用才返回。而异步调用则是立即返回,由系统在数据读读取写入完成时通知应用程序。其实,微软的异步IO函数里,已经提供了这样的机制,主要是通过Event进行通知的。在异步IO操作的每一个函数调用时,都需要传入OVERLAPPED结构,里面就可以设置Event对象,以便接收通知。
在一般的客户端应用程序里,我们一般采用第一种即多线程的模式,因为这样比较简单,也比较灵活。而在要求同时处理大量用户请求的服务端程序里,如果还采用第一种模式,按照每个用户对应一个处理线程的话,一万个并发用户就要开一万个线程,这显然是不可取的。这时候就要使用第二种模式,即异步调用。
对于大量的文件句柄,如果设置不同的Event信号量来进行状态通知,系统消耗太大。于是,微软发明了IO Completion Ports (IOCP),你可以创建一个虚拟的Port,然后把多个文件句柄与之进行关联。这样,所有的通知都会聚集在这个Port上,相当于一个消息队列。然后,创建工作线程池来跟Port进行关联,一旦有通知到达Port(相当于写队列),它就会激活其中一条工作线程进行处理(相当于读队列)。这里,我们可以看到,IOCP就是一个同步对象,用于协调文件句柄和工作线程。
我们可以看到,对于IOCP而言,所有工作线程的代码逻辑都是一样的。为了处理不同的情况,必然需要有很多Switch语句,这种结构对复杂逻辑是一个灾难。但是,对于大量的同类操作,这种方式则是恰当的。因此,IOCP应该主要应用在瓶颈产生的地方,即请求接入,以及数据读取和写入的地方。在这里对数据进行预处理之后(比如转换成内部的Task),后面的代码逻辑就可以对预处理过的数据进行全速处理,而完全不用考虑延迟的问题了。
最后,我们再来看IOCP的字面意思。IO Complete Port,不就是Port(港口)嘛。IO对象(文件句柄和Socket)就像货轮;收发数据即是往来货物;工作线程即装卸车。为什么要有港口,就是集中统一管理的方便!一辆装卸车可以为多个货轮服务,最大的提高了使用效率。
Synchronous and Asynchronous I/O
http://msdn.microsoft.com/en-us/library/aa365683(VS.85).aspx
I/O Completion Ports
http://msdn.microsoft.com/en-us/library/aa365198(VS.85).aspx
Inside I/O Completion Ports
http://technet.microsoft.com/en-us/sysinternals/bb963891.aspx
Windows Sockets 2.0: Write Scalable Winsock Apps Using Completion Ports
http://msdn.microsoft.com/zh-cn/magazine/cc302334(en-us).aspx