完成端口(Completion Port)
- 完成端口是Win32一种核心对象。利用完成端口模型,套接字应用程序能够管理数百个甚至上千个套接字。应用程序创建一个Win32完成端口对象,通过指定一定数量的服务线程,为已经完成的重叠I/O操作提供服务。该模型往往可以达到最好的系统性能。
- 完成端口是真正意义上的异步模型。该模型解决了“one-thread-per-client”的问题。当应用程序需要管理成百上千个套接字,并且希望随着系统安装的CPU数量的增加,应用程序的性能得到提升时,I/O完成端口模型是最好的选择。
服务器线程模型:
- 服务器有两种线程模型:串行模型和并发模型 串行模型单个线程等待客户端的请求。当请求到来时,该线程醒来处理请求。该模型应用于简单的服务器程序。服务器接受的请求比较少,接受的请求能被很快地处理。
- 该模型的缺点在于:当多个客户端同时向服务器发出请求时,这些请求必须依次被接受。例如:两个客户端同时向服务器发出请求,第二个请求必须在第一个请求处理完毕后,才能被接受。 并发模型:单个线程等待客户端请求,当请求到来时,创建新线程来处理请求。等待客户请求的线程继续等待另一个客户端请求。新线程处理完客户端请求后退出。 服务器线程模型为每个客户端创建一个新线程,客户端请求能够很快地处理。由于每个客户端请求都有自己的线程,所以服务器程序的伸缩性比较好。当升级硬件时,服务器程序的性能可以得到提高。
-
并发模型的不足:
-
通过上面的分析,串行模型不适合开发高效的服务器程序,而并发模型比较适合,但是并发模型也存在如下的不足:
-
并不是每个客户端请求都创建一个线程,就一定会提高套接字应用程序的性能。因为当服务器创建许多线程时,系统内核进行线程上下文会花费很多时间,而线程没有足够的时间为客户端服务。
-
并发模型在接受客户端请求后,创建一个新的线程,当服务结束后,使线程退出。当新的客户端请求到来时,再创建新的线程。这样不断地创建和销毁线程,会增加系统的开销。
-
完成端口目标是实现高效的服务器程序,他克服了并发模型的不足。其方法一是为完成端口指定并发线程的数量;二是在初始化套接字时创建一定数量的服务线程,即所谓的线程池。当客户端请求到来时,这些线程立即为之服务。
-
完成端口的理论基础是并行运行的线程数量必须有一个上限。这个数值就是CPU的个数。如果一台机器有两个CPU,那么多于两个可运行的线程就没有意义了。因为一旦运行线程数目超出CPU数目,系统就不得花费时间来进行线程上下文的切换,这将浪费宝贵的CPU周期。
-
完成端口并行运行的线程数目和应用程序创建的线程数量是两个不同的概念。
-
服务器应用程序需要创建多少个服务器线程,一般规律是CPU数目乘以2.例如,单CPU的机器,套接字应用程序应该创建2个线程的线程池。
-
接下来的问题是,完成端口如何实现对线程池的有效管理,使这些服务线程高效运行起来。
-
当系统完成I/O操作后,向服务器完成端口发送I/O completion packet。这个过程发生在系统内部,对应用程序是不可见的。在应用程序方面,此时线程池中的线程在完成端口上排队等待I/O操作完成。如果在完成端口上没有接收到I/O completion packet时,这些线程处于睡吧状态。当I/O completion packet 被送到完成端口时,这些线程按照后进先出(LIFO Last-in-First-out)方式被唤醒。
-
完成端口之所以采用这种方式,其目的是为了提高性能。例如,有3个线程在完成端口上等待,当一个I/O completion packet到达后,队中最后一个线程被唤醒。
-
该线程为客户端完成服务后,继续在完成端口上等待。如果 此时又有一个I/O completion packet 到达完成端口,则该线程线程又被唤醒,为该客户端提供服务。如果完成端口不采用LIFO方式,完成端口唤醒另外一个线程,则必然要进行线程之间的上下文切换。通过使用LIFO方式,还可以使得不被唤醒的线程内存资源从缓存中清除 。
-
在前面讲到的,应用程序需要创建一个线程池,在完成端口上等待。线程池中的线程数目一定大于完成端口并发运行的线程数目,似乎应用程序创建了多余的线程,其实不然,之所以这样做是因为保证CPU尽可能的忙碌。
-
例如,在一台单CPU的计算机上,创建一个完成端口的应用程序,为其制定并发线程数目为1.在应用程序中,创建2个线程在完成端口上等待。假如在一次为客户端服务时,被唤醒的线程因调用Sleep()之类的函数而处于阻塞状态,此时,另外一个I/O completion packet 被发送到完成端口上。完成端口会唤醒另外一个线程为该客户提供服务。这就是线程池中线程数目要大于完成端口指定的并发线程数量的原因。
-
根据上面分析,在某些情况下,完成端口并行运行的线程数量会超过指定数量。但是,当服务线程为客户端完成服务后,在完成端口等待时,并发的线程数量还会下降。
-
总之,完成端口为套接字应用程序管理线程池,避免反复创建线程的开销,同时,根据CPU的数量决定并发线程的数量,减少线程的调度,从而提高服务器程序性能。
与重叠I/O模型比较:
- 重叠I/O模型和完成端口模型相同点在于他们都是异步模型,都可以使得套接字应用程序性能得到改善。
- 重叠I/O模型与完成端口模型相比存在以下不足: 在事件通知方式的套接字应用程序中,使用WSAWaitForMultipleEvents()函数,应用程序最多等待WSA_MAXIMUM_WAIT_EVENTS个事件对象。
- 在Win32 SDK 中,该值为64.作为一个服务器程序,该函数限制了服务器为之提供服务的客户端的数量。 应用程序必须维护一个“事件—套接字—重叠结构”关系表格。根据发生的事件对象,确定套接字和重叠结构。
- 一个套接字可以关联一个、两个或多个事件对象,而事件对象与重叠结构之间保持着一一对应的关系。应用程序管理这个关系表格时,如果出现一点疏漏,就会造成严重的后果。
- 完成端口优点:
-
完成端口实际上一个通知队列。
-
当某项I/O操作完成时,由操作系统向完成端口发送通知包。一方面,这些通知包在完成端口上排队,按照FIFO(First_in_First_out)方式被提取。另一方面,在完成端口上一定数量的现场等待接收通知包,这些线程按照LIFO方式被唤醒。套接字在被创建后,可以在任何时候与某个完成端口进行关联。 对发起重叠操作的数量不存在限制。 支持scalable架构。Scalable系统是指随着RAM、磁盘空间或者CPU个数的增加而能够提升应用程序效能的一种系统。