【C#进阶系列】27 I/O限制的异步操做

上一章讲到了用线程池,任务,并行类的函数,PLINQ等各类方式进行基于线程池的计算限制异步操做。数据库

而本章讲的是如何异步执行I/O限制操做,容许将任务交给硬件设备来处理,期间彻底不占用线程和CPU资源。编程

然而线程池仍然扮演着重要的角色,由于各类I/O操做的结果仍是要由线程池线程来处理。windows

Windows如何执行同步I/O操做数组

既然说道异步I/O操做,那么首先能够先看看同步操做是如何执行。安全

就好比操做硬盘上的一个文件,经过构造一个FileStream对象打开磁盘文件,而后调用Read方法从文件读取数据。服务器

调用Read方法时,线程从托管代码转变为本机/用户模式代码,Read内部调用Win32的ReadFile函数。数据结构

ReadFile分配一个小的数据结构,称为I/O请求包(I/O Request Packet,IRP)。多线程

IRP结构初始化后包含的内容有:文件句柄,文件中的偏移量(从这个位置开始读取字节),一个Byte[]数组的地址,要传输的字节数以及其它常规性内容。异步

而后ReadFile函数将线程从本机/用户模式代码转变为本机/内核模式代码,像内核传递IRP,从而调用Windows内核。根据IRP中的设备句柄,Windows内核知道I/O操做要传送给哪一个硬件设备。async

所以,Windows将IRP传送给恰当的设备驱动程序的IRP队列。每一个设备驱动程序都维护本身的IRP队列,其中包含了机器上运行的全部进程发出的I/O请求。

IRP数据包到达时,设备驱动程序将IRP信息传递给物理硬件设备上安装的电路板。如今,硬件设备将执行请求的I/O操做。

在硬件执行I/O操做期间,发出了I/O请求的线程将无事可作,因此Windows将线程变成睡眠状态,防止它仍然浪费CPU时间。(然而仍然浪费内存,由于它的用户模式栈,内核模式栈,线程环境块和其它数据结构依然在内存中,并且没有东西访问这些内存)。

最终硬件设备会完成I/O操做,而后Windows唤醒线程,将其调度给一个CPU,使它从内核模式返回用户模式,再返回至托管代码。FileStream的Read方法返回一个Int32,指明从文件中读取的字节数,使咱们知道在传给Read的Byte[]中,实际能检索到多少字节。

对于Web服务器而言,这么作的话就坑爹了。能够想象,若是有不少用户请求服务器,获取某文件或数据库的信息,在获取时线程阻塞,等待返回,那么就会建立不少线程,若是用户量足够大,服务器根本就不够用。

而当获取到了信息,大量线程被唤醒,那么此时就存在大量的线程,而CPU内核通常不会不少,因此就会频繁切换上下文,这进一步损害了性能。

Windows如何执行异步I/O操做

基于同步I/O操做在某些场景下的坑爹表现, 固然就须要异步操做来解决了。

依然是那个例子,一样是构造一个FileStream去读取文件,然而如今传递一个FileOptions.Asynchronous标志,告诉Windows但愿用异步方式进行文件读写。

而且如今不是调用Read而是ReadAsync来读取数据。

ReadAsync内部分配一个Task<Int>来表明用于完成读取操做的代码。

而后ReadAsync调用Win32 ReadFile函数。

ReadFile分配IRP,和前面同步操做同样初始化它,而后传递给windows内核。

Windows内核将IRP放到驱动程序队列中,但线程再也不阻塞,而容许返回至你的代码。(这就是异步的好处了)

因此线程能当即从ReadAsync调用中返回。固然此时IRP还没有处理好,因此不能在ReadAsync以后的代码中访问传递的Byte[]中的字节。

ReadAsync以前在内部建立的Task<Int>对象会返回给用户。

可在该对象上调用ContinueWith来登记任务完成时执行的回调方法,而后在回调函数中处理数据。固然也能够用C#的异步函数功能简化代码,以顺序方式写代码(感受就像是执行同步I/O)。

硬件设备处理好IRP后,会将IRP放到CLR的线程池队列,未来某个时候一个线程池线程会提取完成的IRP并/ 成任务的代码,最终要么设置异常(若是发生错误),要么返回结果(本例表明成功读取字节数的一个Int32)

这样一来,Task对象就知道操做在何时完成,代码能够开始运行并安全地访问Byte[]中的数据。

这样不阻塞线程使得资源不至于被过分浪费,同时提升了I/O效率。

C#异步函数

在我写WEB的经历中历来没用过异步函数,却是之前玩了一段事件Unity3D的时候用过。

实际上在上一章执行定时计算限制操做那个小节就已经用过了,把那个例子粘贴过来了:

      static void Main(string[] args)
        {
            asyncDoSomething();
            Console.Read();
        }

        private static async void asyncDoSomething() {
            while (true) {
                Console.WriteLine("time is {0}", DateTime.Now);
                //不阻塞线程的前提下延迟两秒
                await Task.Delay(2000);//await容许线程返回
                //2秒后某个线程会在await后介入并继续循环
            }
        }

这里的asyncDoSomething这个函数就是异步函数。

它有一个很明显的标志,就是用async声明了一下。

异步函数的内部实际上就是使用了Task来实现异步,并且用了一个之前没有提过的概念:状态机。

异步函数,顾名思义会异步执行,并且在await后面的操做A通常也是异步执行,且等操做A执行完了,才会继续执行await那一行语句后面的语句。

写法上像一个正常函数,实际上在其内部用Task的ContinueWith去运行恢复状态机的方法。使Task.Delay(2000)这个线程执行完后,又有一个线程来调用await那行代码以后的代码。

使用异步函数要注意如下几点:

  • 不能将程序的Main函数做为异步函数。另外构造器,属性和事件访问器方法也不能用。
  • 异步函数不能有out和ref参数
  • 不能在catch,finally或unsafe块中使用await操做符
  • 不能在await操做符以前得到一个支持线程全部权或递归的锁,并在await操做符后释放它。这是由于await以前的代码是由一个线程执行,以后的代码由另外一个线程执行
  • 在查询表达式中,await操做符只能在初始from子句的第一个集合表达式中使用,或者在join子句的集合表达式使用。

异步函数的返回类型通常是Task或者Task<某类型>,它们表明函数的状态机完成。(不过也能够像咱们上面的例子同样返回void)

事实上,若是异步函数最后return的一个int值,那么异步函数的返回类型就应该是Task<int>。

通常来说,异步函数都会按规范要求在方法名后附加Async后缀。支持I/O操做的不少类型都提供了Async方法。

在早期版本中,有一个编程模型是使用BeginXxx/EndXxx方法和IAsyncResult接口。

还有一个基于事件的编程模型,提供了XxxAsync方法(不返回Task对象,由于事件都是void)

如今这两个编程模型都已通过时了,建议用新的以Async结尾的函数的编程模型。(不过仍是有一些类由于微软没时间更新,因此这些类只有BeginXxx这种方法)

对于只有BeginXxx和EndXxx的编程模型的类,能够用Task.Factory.FromAsync方法,将BeginXxx和EndXxx分别做为参数传给FromAsync,而后就能够await Task.Factory.FromAsync(BeginXxx,EndXxx,null)的方式,用新得编程模型了。

应用程序与线程处理模型

.NET支持几种不一样的应用程序模型,而每种模型可能引入了它本身的线程处理模型。

控制台应用程序和Windows服务(实际上也是控制台应用程序,只是看不到控制台)没有引入任何线程处理模型。

而GUI应用程序引入了一个线程处理模型。在此模型中,UI元素只能由建立它的线程更新。

在GUI线程中,常常都须要生成一个异步操做,使GUI线程不至于阻塞并中止响应用户输入。但当异步操做完成时,是由一个线程池线程完成Task对象并恢复状态机。

可是当这个线程池线程一旦更新UI元素就会抛出异常,因此线程池线程只能呢个以某种方式告诉GUI线程更新UI元素。

然而FCL定义了一个SynchronizationContext类(同步上下文类)来解决这个问题,简单来讲此类的对象将应用程序模型和线程处理模型链接起来。

做为开发人员一般不须要了解这个类,等待一个Task时会获取调用线程的SynchronizationContext对象,线程池完成Task后,会使用该SynchronizationContext对象,确保为应用程序模型使用正确的线程处理模型。

因此当GUI线程等待一个Task时,await操做符后面的代码保证在GUI线程上执行,使代码能正确执行。

Task提供了一个ConfigureAwait方法,向其传递true就至关于没有调用方法,传递false则await操做符就不查询调用线程的SynchronizationContext对象。当线程池结束Task时会直接完成,await操做符后面的代码经过线程池线程执行。

以异步方式进行I/O操做

以前虽然介绍了异步方式进行I/O操做,实际上那些操做是在内部用另外一个线程模拟异步操做。这个额外的线程也会影响到性能。

若是在建立FileStream对象时,指定FileOptions.Asynchronous标志,表示以同步仍是异步方式来通讯。

在这个模式下,调用Read,实际上内部也是用异步方式来模拟同步实现。(而实际上若是指定了异步,那么就用ReadAsync,若是是同步,就用Read,这样才能获得最好的性能)

 

PS:

本章实际上的含金量比我写的这些多很多,能力有限无法彻底写出来。(信息量较大,我本身都有点迷糊,估计搞完这一轮,还要回过头来再看看多线程这块)

特别是在异步函数的状态机那里,本书介绍的很详细,然而我并无写太多。

主要是做者用了一大片的代码来解释,而本人实在懒得抄。

不过相信中心思想仍是提炼出来了,实际上使用了任务,而后功能也至关于把await后面的代码ContinueWith了。

相关文章
相关标签/搜索