本文源于笔者在公司内部的一个分享。几月前为了搞懂这些知识花费了大量的时间调查研究,最终的理解算是全面而透彻了。而如今学习其余技术时,间或会遇到与此相似的话题,因而把先前的总结记录下来,以做备忘,并启发本身举一反三。文中图片都取自当时的Slides。数据库
服务器的计算分为IO计算和CPU计算。IO计算指计算任务中以IO为主的计算模型,好比文件服务器、邮件服务器等,混合了大量的网络IO和文件IO;CPU计算指计算任务中没有或不多有IO,好比加密/解密,编码/解码,数学计算等等。编程
须要关心的是IO计算,通常的网络服务器程序每每伴随着大量的IO计算。提升性能的途径在于要避免等待IO 的结束,形成CPU空闲,要尽可能利用硬件能力,让一个或多个IO设备与CPU并发执行。服务器
另外一方面,CPU密集的计算是咱们没法控制的。若是是CPU计算出现了瓶颈,那只能给服务器增长CPU,或者增长服务器。而IO操做,其实是空等别的硬件,这里面的优化就大有可为。网络
大部分Web服务的大部分操做都是IO密集型的。无非是读磁盘、查数据库、访问网络调用别的API,而这些都是经过操做系统的IO。
为了下降请求等待时间,对每一个请求都创建一个线程来处理能够吗?这是不少早期服务器的方式。而这种方式存在很大的问题。多线程
首先,建立线程/进程和销毁线程/进程的代价很是高昂,尤为是在服务器采用TCP“短链接”方式或UDP方式通信的状况下,例如,HTTP协议中,客户端发起一个链接后,发送一个请求,服务器回应了这个请求后,链接也就被关闭了。若是采用经典方式设计HTTP服务器,那么过于频繁地建立线程/销毁线程对性能形成的影响是很恶劣的。并发
线程还会占用内存。线程的内核对象占几M到几时M。普通的计算机上千个线程就会耗尽内存。app
线程切换也会消耗时间,最终致使服务器性能急剧降低。若是客户端并发请求量很高,同一时刻有不少客户端等待服务器响应的状况下,将会有过多的线程并发执行,频繁的线程切换将用掉一部分计算能力。负载均衡
对于一个须要应付同时有大量客户端并发请求的网络服务器来讲,线程池是惟一的解决方案。线程池不光可以避免频繁地建立线程和销毁线程,并且可以用数目不多的线程就能够处理大量客户端并发请求。框架
只有线程池,而继续同步等待IO请求等于没用,可达到的吞吐量将很是有限。异步IO则可让线程池中的每一个线程用很短的时间处理请求中的计算任务,而后把任务交给IO,线程继续处理别的请求。当IO返回时,一个线程池中的线程再被指派来完成剩下的工做,直到完成请求的响应。如此一来,就能够压榨出最大的硬件性能。
这也是为什么Nodejs将异步做为核心卖点,各大主流语言和框架都有相似的异步支持和async/await句式了。异步
Windows IO的基本过程以下:
其中要用到中断和APC,简要介绍一下:
CPU在执行指令的间隙能够被中断打断,进入内核态。内核态共享一个地址空间,没有进程线程之分。CPU将执行中断分发例程,保存执行现场,执行中断服务例程,再恢复现场,继续执行原来的的指令。
中断还有优先级的概念。处于高优先级时,低优先级的中断不能被处理,直到CPU中断优先级降下来。
是操做系统提供的一种异步回调机制。能够把一段任务代码放到某个用户线程的内核结构的某个队列中,程序正常运行时不会执行。只有当线程发起某些调用,使本身成为Alertable
(可唤醒)时,才会检查APC队列,把其中的任务都执行了。这些进入可唤醒状态的调用有:
WaitForSingleObjectEx()
SleepEx()
等等。再来看第一幅图。经过SYSCALL
指令进行中断调用,进入内核态。内核调用驱动程序去完成IO。而调用是否当即返回(异步),仍是阻塞等待(同步),取决于传入的参数。另外一张.NET IO的图则能够提供更多理解。
同步模式
CreateFile()
, ReadFile()
, WriteFile()
等API的默认方式就是同步阻塞。异步模式
Overlapped
结构体
CreateFile(..., FILE_FLAG_OVERLAPPED,...)
打开文件时加入此标志启动异步模式,API调用会当即返回。GetOverlappedResult/WaitForMultipleObjects
等待Overlapped结构体,能够等待IO完成。APC完成例程
WriteFile(..., lpCompletionRoutine)
,传入一个IO完成例程,内核会在IO完成时将此例程放入线程APC队列。此线程再在适时调用SleepEx()
等进入可唤醒状态,执行IO完成例程。IOCP(IO 完成端口)
CreateIoCompletionPort()
建立IO完成端口。GetQueuedCompletionStatus()
等待IO完成端口上面的IO完成信号。IO完成端口会保证只触发合适数量的线程(约等于CPU核心数)来处理IO完成后的工做。PostQueuedCompletionStatus()
来关闭IO完成端口。以上讲解对于不了解Windows核心编程的读者来讲确定没法造成什么认识。如下经过一个例子来介绍这几种模式。
这里用一个Winsock2(Socket的Windows版)的程序举例。只做图示,具体代码未必彻底契合。
红色表示阻塞。
GetOverlappedResult()
等待多个Overlapped
结构,有完成的IO就解决处理,而后继续等待。一个线程就能够处理全部请求!
惋惜它也有缺点。等待多个对象相对于WaitForMultipleObjects()
,而这个函数有64个等待对象的上限。因此只能用多线程来等待。当请求量很大时,线程数量也会变大。
缺点:线程执行的主动性不在本身;负载均衡不易控制。现在已极少使用。
能够把IOCP理解成一种内核同步机制。