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

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

然而线程池仍然扮演着重要的角色,因为各种I/O操作的结果还是要由线程池线程来处理。

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操作要传送给哪个硬件设备。

因此,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();//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了。

【C#进阶系列】27 I/O限制的异步操作的更多相关文章

  1. 【C#进阶系列】26 计算限制的异步操作

    什么是计算限制的异步操作,当线程在要使用CPU进行计算的时候,那么就叫计算限制. 而对应的IO限制就是线程交给IO设备(键鼠,网络,文件等). 第25章线程基础讲了用专用的线程进行计算限制的操作,但是 ...

  2. C#进阶系列 ---- 《CLR via C#》

      [C#进阶系列]30 学习总结 [C#进阶系列]29 混合线程同步构造 [C#进阶系列]28 基元线程同步构造 [C#进阶系列]27 I/O限制的异步操作 [C#进阶系列]26 计算限制的异步操作 ...

  3. Wireshark入门与进阶系列(二)

    摘自http://blog.csdn.net/howeverpf/article/details/40743705 Wireshark入门与进阶系列(二) “君子生非异也,善假于物也”---荀子 本文 ...

  4. Ext JS学习第十六天 事件机制event(一) DotNet进阶系列(持续更新) 第一节:.Net版基于WebSocket的聊天室样例 第十五节:深入理解async和await的作用及各种适用场景和用法 第十五节:深入理解async和await的作用及各种适用场景和用法 前端自动化准备和详细配置(NVM、NPM/CNPM、NodeJs、NRM、WebPack、Gulp/Grunt、G

    code&monkey   Ext JS学习第十六天 事件机制event(一) 此文用来记录学习笔记: 休息了好几天,从今天开始继续保持更新,鞭策自己学习 今天我们来说一说什么是事件,对于事件 ...

  5. C#进阶系列——WebApi 接口返回值不困惑:返回值类型详解

    前言:已经有一个月没写点什么了,感觉心里空落落的.今天再来篇干货,想要学习Webapi的园友们速速动起来,跟着博主一起来学习吧.之前分享过一篇 C#进阶系列——WebApi接口传参不再困惑:传参详解  ...

  6. C#进阶系列——WebApi 接口参数不再困惑:传参详解

    前言:还记得刚使用WebApi那会儿,被它的传参机制折腾了好久,查阅了半天资料.如今,使用WebApi也有段时间了,今天就记录下API接口传参的一些方式方法,算是一个笔记,也希望能帮初学者少走弯路.本 ...

  7. C#进阶系列——WebApi 接口测试工具:WebApiTestClient

    前言:这两天在整WebApi的服务,由于调用方是Android客户端,Android开发人员也不懂C#语法,API里面的接口也不能直接给他们看,没办法,只有整个详细一点的文档呗.由于接口个数有点多,每 ...

  8. C#进阶系列——WebApi 跨域问题解决方案:CORS

    前言:上篇总结了下WebApi的接口测试工具的使用,这篇接着来看看WebAPI的另一个常见问题:跨域问题.本篇主要从实例的角度分享下CORS解决跨域问题一些细节. WebApi系列文章 C#进阶系列— ...

  9. C#进阶系列——WebApi 身份认证解决方案:Basic基础认证

    前言:最近,讨论到数据库安全的问题,于是就引出了WebApi服务没有加任何验证的问题.也就是说,任何人只要知道了接口的url,都能够模拟http请求去访问我们的服务接口,从而去增删改查数据库,这后果想 ...

  10. C#进阶系列——WebApi 异常处理解决方案

    前言:上篇C#进阶系列——WebApi接口传参不再困惑:传参详解介绍了WebApi参数的传递,这篇来看看WebApi里面异常的处理.关于异常处理,作为程序员的我们肯定不陌生,记得在介绍 AOP 的时候 ...

随机推荐

  1. 【腾讯Bugly干货分享】Android Linker 与 SO 加壳技术

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57e3a3bc42eb88da6d4be143 作者:王赛 1. 前言 Andr ...

  2. ,net core mvc 文件上传

    工作用到文件上传的功能,在这个分享下 ~~ Controller: public class PictureController : Controller { private IHostingEnvi ...

  3. 熊乐:H3 BPM为加速企业流程管理提供源动力

    近日,在北京·金隅喜来登酒店,H3 BPM以"让天下没有难用的流程"为主题,正式发布H3 BPM10.0版本.全新的业务流程管理系统在易用性方面大大提升,并且全面支持Java与.N ...

  4. Firebug中调试中的js脚本中中文内容显示为乱码

    Firebug中调试中的js脚本中中文内容显示为乱码 设置 页面 UFT-8 编码没用, 解决方法:点击 "Firebug"工具栏 中的"选项"---" ...

  5. Configure a bridged network interface for KVM using RHEL 5.4 or later?

    environment Red Hat Enterprise Linux 5.4 or later Red Hat Enterprise Linux 6.0 or later KVM virtual ...

  6. Linux C语言解析并显示.bmp格式图片

    /************************* *bmp.h文件 *************************/ #ifndef __BMP_H__ #define __BMP_H__ # ...

  7. 【腾讯Bugly干货分享】动态链接库加载原理及HotFix方案介绍

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57bec216d81f2415515d3e9c 作者:陈昱全 引言 随着项目中动 ...

  8. 架构之路(八)从CurrentUser说起

    CurrentUser,也就是当前用户,这是我们系统中大量使用的一个概念. 确认当前用户 当然,我们利用的是cookie:用户的ID存放在cookie中,服务器端通过cookie中的Id,查找数据库, ...

  9. DevExpress学习系列(控件篇):GridControl的基本应用

    一般属性设置 不显示分组框:Gridview->Option View->Show Group Panel=false 单元格不可编辑:gridcontrol -->gridview ...

  10. Azure 部署 Asp.NET Core Web App

    在云计算大行其道的时代,当你在部署一个网站时,第一选择肯定是各式各样的云端服务.那么究竟使用什么样的云端服务才能够以最快捷的方式部署一个 ASP.NET Core 的网站呢?Azure 的 Web A ...