WinSock 异步I/O模型-4
2021-04-17 18:27
- 服务器有两种线程模型:串行模型和并发模型 串行模型单个线程等待客户端的请求。当请求到来时,该线程醒来处理请求。该模型应用于简单的服务器程序。服务器接受的请求比较少,接受的请求能被很快地处理。
- 该模型的缺点在于:当多个客户端同时向服务器发出请求时,这些请求必须依次被接受。例如:两个客户端同时向服务器发出请求,第二个请求必须在第一个请求处理完毕后,才能被接受。 并发模型:单个线程等待客户端请求,当请求到来时,创建新线程来处理请求。等待客户请求的线程继续等待另一个客户端请求。新线程处理完客户端请求后退出。 服务器线程模型为每个客户端创建一个新线程,客户端请求能够很快地处理。由于每个客户端请求都有自己的线程,所以服务器程序的伸缩性比较好。当升级硬件时,服务器程序的性能可以得到提高。
-
并发模型的不足:
-
通过上面的分析,串行模型不适合开发高效的服务器程序,而并发模型比较适合,但是并发模型也存在如下的不足:
-
并不是每个客户端请求都创建一个线程,就一定会提高套接字应用程序的性能。因为当服务器创建许多线程时,系统内核进行线程上下文会花费很多时间,而线程没有足够的时间为客户端服务。
-
并发模型在接受客户端请求后,创建一个新的线程,当服务结束后,使线程退出。当新的客户端请求到来时,再创建新的线程。这样不断地创建和销毁线程,会增加系统的开销。
-
完成端口目标是实现高效的服务器程序,他克服了并发模型的不足。其方法一是为完成端口指定并发线程的数量;二是在初始化套接字时创建一定数量的服务线程,即所谓的线程池。当客户端请求到来时,这些线程立即为之服务。
-
完成端口的理论基础是并行运行的线程数量必须有一个上限。这个数值就是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的数量决定并发线程的数量,减少线程的调度,从而提高服务器程序性能。