上一章讲到了用线程池,任务,并行类的函数,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. 【知识必备】RxJava+Retrofit二次封装最佳结合体验,打造懒人封装框架~

    一.写在前面 相信各位看官对retrofit和rxjava已经耳熟能详了,最近一直在学习retrofit+rxjava的各种封装姿势,也结合自己的理解,一步一步的做起来. 骚年,如果你还没有掌握ret ...

  2. GJM : C#设计模式(1)——单例模式

    感谢您的阅读.喜欢的.有用的就请大哥大嫂们高抬贵手"推荐一下"吧!你的精神支持是博主强大的写作动力以及转载收藏动力.欢迎转载! 版权声明:本文原创发表于 [请点击连接前往] ,未经 ...

  3. Flexible 弹性盒子模型之CSS flex-grow 属性

    实例 让第二个元素的宽度为其他元素的三倍: div:nth-of-type(1){flex-grow:1;} div:nth-of-type(2){flex-grow:3;} div:nth-of-t ...

  4. Android Studio切换为eclipse的快捷键之后还是有区别的部分快捷键

    Android Studio Eclipse 把代码提示换成了Class Name Completion, 快捷键是Ctrl+Alt+Space(空格键). 代码提示快捷键Alt+/,         ...

  5. Android中使用ExpandableListView实现微信通讯录界面(完善仿微信APP)

    之前的博文<Android中使用ExpandableListView实现好友分组>我简单介绍了使用ExpandableListView实现简单的好友分组功能,今天我们针对之前的所做的仿微信 ...

  6. Android Bitmap 和 ByteArray的互相转换

    Android Bitmap 和 ByteArray的互相转换 移动平台图像处理,需要将图像传给native处理,如何传递?将bitmap转换成一个 byte[] 方便传递也方便cpp代码直接处理图像 ...

  7. 搭建一套自己实用的.net架构(3)续 【ORM Dapper+DapperExtensions+Lambda】

    前言 继之前发的帖子[ORM-Dapper+DapperExtensions],对Dapper的扩展代码也进行了改进,同时加入Dapper 对Lambda表达式的支持. 由于之前缺乏对Lambda的知 ...

  8. ubuntu 14.10 lts 64-bits环境下使用Android Studio

    距离google发布android studio 1.0正式版已经两个月左右了.由于一直习惯使用eclipse+ADT的模式,而且曾在windows下试用一次Android Studio预览版,感觉卡 ...

  9. CSharpGL(26)在opengl中实现控件布局/渲染文字

    CSharpGL(26)在opengl中实现控件布局/渲染文字 效果图 如图所示,可以将文字.坐标轴固定在窗口的一角. 下载 CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入( ...

  10. Android点击列表后弹出输入框,所点击项自动滚动到输入框上方

    使用微信的朋友圈会发现,点击某一条评论后输入框会弹出来,然后所点击的那一项会自动地滚动到输入框上方的位置,这样如果开始所点击的评论在屏幕很下方的话,就不会被输入框遮住,虽然微信这一点在我的MX2频繁点 ...