I/O完成端口,Windows下高性能服务器基础

Windows设计目标是实现一个安全,健壮的操作系统,能够运行各种各样的程序来为用户提供服务,我们现在就来了解一下以前Windows系统下服务器的架构:

1.串行模型,英文全称Serial model,就是一个线程等待一个客户发出请求,当请求到达的时候,线程会被唤醒去对客户的请求进行处理。

2.并发模型,英文全称Concurrent model,就是一个线程等待一个客户请求,当一个客户请求到达的时候就创建一个新的线程来处理客户请求,当新的线程在处理客户请求的时候,原来的线程会进入下一次循环等待另一个客户请求,当处理客户请求的线程完成客户请求的时候就会结束这个线程。

串行模型缺点很明显,就是它不能很好同时处理多个请求,试想如果两个客户请求同时到达,那么该模型一次只能处理一个,第二个客户请求必须等待第一个请求处理的结束,所以使用串行模型来实现服务器的话,那么电脑里面CPU多核心就浪费了呀,如果客户请求很少很少的话就可以考虑使用串行模型来实现服务器。

那么串行模型的缺点我们明白了,那么并发模型的缺点呢?并发模型实现的服务器中每一个客户请求都由一个独立的线程去处理,而等待客户请求到来的线程就工作量很少,大多数时间都是处于睡眠状态,当客户请求到达的时候该线程会被唤醒并且创建一个新的线程来处理客户请求,所以服务器会具备很好的伸缩性,能够发挥CPU多核的优势。但是缺点呢?试想一下服务器让一个客户请求独占一个线程,但是如果CPU只有四个核心但是客户请求一次性到达几十个哪会怎么办呢?会创建几十个线程!所以Windows内核在可运行的各个线程之间进行线程的上下文切换而花费太多时间而使各个线程都没有太多时间去完成处理客户请求的任务了。而且每一个客户请求都创建一个线程去处理?太奢侈了呀,虽然与创建一个进程相比创建一个线程开销要少很多,但是对应要实现高性能的服务器来说,这个开销也是不可忽视的呀,为了让Windows系统变成一个出色的服务器环境,Microsoft的解决方案就是I/O完成端口内核对象。

I/O完成端口理论就是能够并发运行的线程数量有一个上限,也就是说一次性有500个客户请求到达的话,不可能创建500可运行的线程,而是根据电脑配置或者设置去创建指定n个线程而已。那这个可运行线程的数量上限是多少才合适呢?我们要记住的一个准则就是不要让系统花费时间去执行线程的上下文切换而浪费宝贵的CPU周期。如果在服务器开始的时候创建一个线程池,让线程池中的线程一直保持可运行状态,那么服务器的性能就能得到提高,那么线程池中应该有多少个线程呢?经验法则就是去CPU核心乘以2,就是说CPU有两个核心的话那么线程池中就应该要有4个线程,为什么呢?我似乎没说过线程池中的线程每一个都会同时执行吧,所以不用担心线程的上下文切换而浪费CPU周期,I/O完成端口的设计初衷就是配合线程池实现服务器的高性能。

为了创建一个I/O完成端口,我们应该调用函数CreateIoComletionPort(),

函数原型 HANDLE CreateIoComletionPort(HANDLE hFile,HANDLE hExistingComletionPort,ULONG_PTR ComletionKey,DWORD dwNumberOfConcurrentThreads);

这个函数不仅会创建一个I/O完成端口而且还会将一个设备与一个I/O完成端口关联起来。第一个参数是一个设备的句柄第二个参数是一个I/O完成端口的句柄,第三个参数称为完成键,而第四个参数就是告诉I/O完成端口在同一个时间最多能有多少个线程可以运行,如果最后一个参数传人的是NULL的话,那么I/O完成端口会使用默认值(主机CPU核心数量)。

当我们创建一个I/O完成端口的时候系统内核实际上会创建五个不同的数据结构,分别是设备列表,I/O完成队列,等待线程队列,已释放线程队列,已暂停线程队列。第一个数据结构是设备列表,表示与该端口关联起来的设备对象,将设备关联到I/O完成端口也是调用函数CreateIoComletionPort()函数,当然设备可以是文件,套接字,邮件槽,管道等,每一次一个设备与I/O完成端口关联起来的时候,系统会将这些信息追加到设备列表中。

第二个数据结构是I/O完成队列,当设备的一个异步I/O请求完成的时候,系统会检查该该设备是否与一个I/O完成端口关联,如果该设备与I/O完成端口关联,那么系统会将已经完成的I/O请求追加到I/O完成端口的I/O完成队列中的结尾,这个队列里面的每一项都包含有属于自己的信息:已传输字节数,完成键的值(就是参数ComletionKey),一个指向I/O请求的OVERLAPPED结构指针,一个错误码。

再回到线程池的讨论,线程池中的所有线程都应该执行同一个函数,一般线程函数最开始初始化完成后就会进入一个循环,在循环内部线程要将自己切换到睡眠状态而不去花费CPU资源,等待设备I/O请求进入I/O完成队列,而这两个步骤可以调用函数GetQueuedComletionStatus()函数来实现。

函数原型  BOOL GetQueuedComletionStatus(HANDLE hComletionPort,PDWORD pdwNumberOfBytesTransferred,PULONG_PTR pComletionKey,OVERLAPPED** ppOverlapped,DWORD dwMilliseconds);

第一个参数表示线程希望对哪一个I/O完成端口进行监控。函数GetQueuedComletionStatus()函数就是将调用线程切换到睡眠状态等指定的I/O完成端口的I/O完成队列出现一项才会返回,然后线程退出睡眠而开始执行下面的代码。然而该函数超出等待时间的话也会返回而退出睡眠状态,所以应该判断一下该函数是哪一种返回。

第三个数据结构是等待线程队列,当线程池中的每一个线程调用函数GetQueuedCimpletionStatus()的时候,调用线程的pid标识符就会被添加到这个等待线程队列里面,让I/O完成端口内核对象始终都知道有哪些线程当前正在等待对已完成I/O请求进行处理。当I/O完成队列里面出现一项的时候,I/O完成端口会唤醒等待线程队列里面的一个线程进行处理,这个被唤醒的线程会得到很多信息:已传输字节数,完成键,overlapped结构地址,当然这些信息都是通过传给函数GetQueuedComletionStatus()函数的pdwNumberOfByteTransferred,pComletionKey,ppOverlapped参数来返回给线程的。提醒一下唤醒等待线程队列的线程的顺序是按后入先出的方式进行的,也就是栈的数据结构一样,也就是说如果每一次I/O完成队列里面都是出现一项而已的话,那么每一个都是唤醒等待线程队列里面的同一个线程,就是最后一个进入等待线程队列的线程。

我们还要考虑一个问题・_・?如果I/O完成端口只运行同时运行指定数量的线程,为了避免线程的上下文切换而浪费CPU周期,但是为什么前面我说最好线程池的线程数量是是CPU核心数量乘以2呢?也就是为什么要在线程池里面创建那么多线程呢?尽管线程池里面没有工作的线程都处于睡眠状态。我们现在举个例子,如果我们的电脑里面的CPU有4个核心,我们创建了一个I/O完成端口,而且设置最多同时4个线程得到执行,我们还创建了一个线程池,让线程池里面有8个线程,看起来我们似乎创建了4个多余的线程,因为I/O完成端口同时只能运行4个线程,那么另外那4个线程有什么用处呀?那就要介绍到第四个和第五个数据结构了,那就是已释放线程列表和已暂停线程列表,那我们再次假设,如果I/O完成队列里面突然来了很多项需要处理,那么I/O完成端口就会让等待线程队列里面的线程去处理,所以就同时唤醒4个线程去处理(我们设置的),但是发生了点情况:其中有一个线程调用了函数(WaitForSingleObject,sleep等)导致线程进入了等待状态,那么I/O完成端口会检测到这个情况,这个时候会更新内部的数据结构,把进入等待的那一个线程pid标识符从已释放线程队列里面移除,然后添加到已暂停线程队列里面。所以现在有三个线程正在运行,一个线程进入等待,还有另外四个线程处于睡眠状态,可是我们设置最大能同时运行的线程数量是4呀,所以I/O完成端口会让处于睡眠状态的线程其中一个退出睡眠状态,从I/O完成队列里面取一项出来处理。也就是说现在四个线程正在运行,一个线程进入等待,还有三个线程正在睡眠状态。我想聪明的你肯定知道原理了,就是线程池里面多创建出来的那四个线程是个替补队员,如果正式队员遇到事情要离开或者是受伤了,替补队员就上场,一旦正式队员回来了或者是康复了,那么这个替补队员做完自己在场上该做的任务就该退场了。(没有指定哪个线程是正式队员哪个线程是替补队员的,只是最好进入等待线程队列的线程会被先唤醒去处理)。

那我们再来思考一个问题・_・?继续上面的例子,有四个线程正在运行,一个线程进入等待,还有三个线程处于睡眠状态,那么如果进入等待的那个线程退出了等待而进入了运行状态,那岂不是有5个正在运行的线程了而导致线程切换上下文而浪费CPU周期了?的确,但是这个情况下I/O完成端口不会再唤醒任何线程,如果有一个线程最快完成了处理,那很遗憾I/O完成端口会让它进入睡眠状态,在这个时候正在运行的线程数量由变成了4个。

I/O完成端口也是一种在线程间进行通讯不错的技术,要使用这个技术,那还有学习一个函数PostQueuedComletuonStatus(),

函数原型    BOOL PostQueuedComletionStatus(HANDLE hComletionPort,DWORD dwNumBytes,ULONG_PTR ComletionKey,OVERLAPPED* pOverlapped);

这个函数用来将一个I/O通知添加到I/O完成队列中,有什么用还不知道?线程池中的线程可以使用这个函数构建一个特殊的ComletionKey参数啊,不同线程之间进行判断在做出相应动作,还可以构建一个特殊ComletionKey让全部线程体面地结束(服务器关闭的时候呀)。

好吧就到这,下次在实现一个I/O完成端口网络服务器的例子供参考吧。

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值