IOCP异步优化

时间:2022-06-25
本文章向大家介绍IOCP异步优化,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一、内存操作和IO操作

在计算机运行执行程序的世界里,从如何得到处理结果分成两大类:

1. 内存操作: CPU在内存里面完成计算,然后得到处理结果。

2. IO操作: CPU会把内存中的程序委托给其他的网络、磁盘等驱动程序,让这些外部的驱动程序来进行具体的处理,处理完成以后再返回给内存程序。对于这两类操作的优化方式是不一样的。内存操作的特点是占用CPU资源,CPU不断的计算。对于内存密集型的操作(Compute-Bound Operation)的优化,我们可以把一个大任务拆分成多个互不影响的子任务,那么就能让多个CPU同时参与运算,最后合并子任务的结果,所花的时间自然就少了。所以内存密集型的操作(Compute-Bound Operation)的优化有一个前提:超线程、多核、甚至是真正的多个CPU的计算机能够同时运行多个线程,对于只有一个CPU的计算机不适合。多线程之间的状态切换是需要额外的CPU资源的。IO操作的特点是基本不占用CPU资源,但是它会占用当前的工作者线程,并使其进入等待状态,等待IO完成的处理结果,然后在继续执行。但是在ASP.NET这种天然多线程的环境里,CLR线程池容量是有上限的,这个上限也代表了应用程序最多可以同时执行的请求数量。如果我们CLR线程池的所有线程都进入了IO等待状态,当再有新用户进来,我们的服务就停止响应了。目前我们IO操作的缺点是当前工作者线程同步等待IO,任何IO处理都会霸占一条工作者线程。所以对于IO密集型的操作(IO-Bound Operation)的优化,我们的思路是使用IOCP(I/O Completion Port)。IOCP翻译了中文是IO完成端口,它是一种异步形态,原理是这样的:当前工作者线程在进行IO处理时,委托给某个设备驱动程序,然后自己返回线程池,当IO完成后,OS会通过IOCP提醒CLR它工作已经完成,当CLR接收到通知后,会唤醒一个I/O线程并且运行用户的回调。

I/O线程:是CLR线程池中预先保留出来的部分线程,这部分线程的作用是为了分发从IOCP中的回调。I/O线程由CLR调用。所以通常情况下,开发者并不会直接用到它。工作者线程和I/O线程区别:它们都是普通的线程,但是CLR线程池中区分它们的目的是为了避免线程都去处理I/O回调而被耗尽,从而引发死锁。关于CLR线程池的细节可以看:线程池的作用和CLR线程池

二、IOCP异步优化

ASP.NET天生就是多线程的运行环境,所以内存密集型的操作(Compute-Bound Operation),我们推荐单线程运算为原则。如果我们在具体业务逻辑里运用了多线程,也意味着系统将对多线程之间的状态切换产生额外的开销。从而加重了服务器的负担。

在IO密集型的操作(IO-Bound Operation)中,我们推荐使用IOCP模式。当执行I/O操作的时候,无论是同步I/O操作还是异步I/O操作,都会调用的Windows的API方法,比如,当读取文件的时候,调用ReadFile函数。该方法会将你的当前线程从用户态转变成内核态,会生成一个I/O请求包,并且初始化这个请求包,这个包中包含一个文件句柄,一个偏移量和一个Byte[]数组。ReadFile会向内核传递,根据这个请求包,windows内核知道需要将这个I/O操作发送给哪个硬件设备。这些I/O操作会进入设备自己的处理队列中,该队列由这个设备的驱动程序维护。

如果此时是同步I/O操作,那么在硬件设备操作I/O的时候,发出I/O请求的线程由于无事可做被windows变成睡眠状态,当硬件设备完成操作后,再唤醒这个线程。这种方式非常直接,但是性能不高,如果请求数很多,那么休眠的线程数也很多,浪费了大量资源。

如果是异步I/O操作,.Net中异步的I/O操作为BeginXXX的形式。该方法在Windows把I/O请求包发送到设备的处理队列后就返回了。同时,在调用异步I/O操作的时候,即调用BeginXXX方法的时候,需要传入一个委托,该委托方法会随着I/O请求包一路传递到设备的驱动程序。在设备处理完I/O请求包后,将该委托再放到CLR线程池中的I/O线程队列里。之前说到过,在CLR内部维护了一个IOCP(I/O completion port),它提供了处理多个异步I/O请求的线程模型,可以把这个IOCP看做是一个消息队列,当一个进程创建了一个IOCP,即创建了一个队列。当异步I/O请 求完成时,设备驱动程序就会生成一个I/O完成包,将它按照FIFO方式排队列入该完成端口。之后,会由I/O线程提取完成I/O请求包,并调用之前的委托。注意:异步调用服务时,回调函数都是运行于CLR线程池的I/O线程当中。

具体的在.NET的代码实例:

static void Main(string[] args)
{
    WebRequest request = HttpWebRequest.Create("http://www.baidu.com");
    request.BeginGetResponse(HandleAsyncCallback, request);
}
static void HandleAsyncCallback(IAsyncResult ar)
{
    WebRequest request = (WebRequest)ar.AsyncState;
    WebResponse response = request.EndGetResponse(ar);
    // more operations...
}

IOCP中有2个队列,一个是先进先出的队列,存放的是IO完成包,即已经完成的IO操作需要执行回调方法,因此先进先出的方式是非常公平的。

还有一个队列是线程队列,IOCP会预分配一些线程在这个队列中,这样会比即时创建线程处理I/O请求速度更快。这个队列是后进先出的,好处是下一个请求的到来可能还是用之前的线程来处理,就不需要进行线程上下文切换,提高了性能。

在《Pro .NET Performance》中有如下一个示意图: