第二十八章 I/O限制的异步操作
目录
28.1 Windows如何执行I/O操作
28.2 C#的异步函数
28.3 编译器如何将异步函数转换成状态机
28.4 异步函数扩展性
28.5 异步函数和事件处理程序
28.6 FCL的异步函数
28.7 异步函数和异常处理
28.8 异步函数的其他功能
28.9 应用程序及其线程处理模型
28.10 以异步方式实现服务器
28.11 取消I/O操作
28.12 有的I/O操作必须同步进行
28.13 I/O请求优先级
异步执行I/O限制操作,允许将任务交由硬件设备处理,期间完全不占用线程和CPU资源。
28.1 Windows如何执行I/O操作
同步I/O操作:程序通过构造一个FIleStream对象来打开磁盘文件,然后调用Read方法从文件中读取数据。调用FileStream的Read方法时,你的线程从托管代码转变为本机/用户模式代码,Read内部调用Win32ReadFile函数。ReadFile分配一个小的数据结构,称为I/O请求包(IRP)。IRP结构初始化后包含的内容有:文件句柄,文件中的偏移量(从这个位置开始读取字节),一个Byte[]数据的地址(数组用读取的字节来填充),要传输的字节数据以及其他常规性内容。然后,ReadFile将你的线程从本机/用户模式代码转变成本机/内核模式代码,向内核传递IRP数据结构,从而调用Windows内核。根据IRP中的设备句柄,Windows内核知道I/O操作要传送给哪个硬件设备。因此,Windows将IRP传送给恰当的设备驱动程序的IRP队列。每个设备驱动程序都维护者自己的IRP队列,其中包含了机器上运行的所有进程发出的I/O请求。IRP数据包到达时,设备驱动程序将IPR信息传给物理硬件设备上安装的电路板。现在,硬件设备将执行请求的I/O操作。在硬件设备执行I/O操作期间,发出了I/O请求的线程将无事可做,所以Windows将线程变成睡眠状态,防止它浪费CUP时间。最终,硬件设备会完成I/O操作。然后,Windows会唤醒你的线程,把它调度给一个CPU,使它从内核模式返回用户模式,再返回至托管代码。FileStream的Read方法现在放回一个Int32,指明从文件中读取的实际字节数,使你知道在传给Read的Byte[]中,实际能检索到多少个字节。
I/O异步操作:构造FileSteam对象,传递一个FileOptions.Asynchronous标识, 告诉Windows我希望文件的读/写操作以异步方式进行。调用ReadAsync从文件中读取数据,它内部分配一个Task<Int32>对象来代表用于完成读取操作的代码。然后调用Win32ReadFile函数。ReadFIle分配IRP,和前面的同步操作一样初始化它,然后把它传给Windows内核。Windows把IRP添加到硬盘驱动程序的IRP队列中。但线程不再阻塞,而是允许返回至你的代码。可在Task<Int32>上调用ContinueWith来登记任务完成时执行的回调方法,然后在回调方法中处理数据。硬件设备处理好IRP后,会将完成的IRP放到CLR的线程池队列中。将来某个时候,一个线程池线程会提取完成的IRP并执行执行完成任务的代码,最终要么设置异常,要么返回结果。
CLR在初始化时创建一个I/O完成端口。当你打开硬件设备时,这些设备可以和I/O完成端口关联,使设备驱动程序知道将完成的IRP送到哪儿。
28.2 C#的异步函数
将方法标记为async,编译器就会将方法的代码转换成实现了状态机的一个类型。这就允许线程执行状态机中的一些代码并返回,方法不需要一直执行到结束。
异步函数限制:
不能讲应用程序的Main方法转变成异步函数。另外,构造器,属性访问器方法和事件访问器方法不能转变成异步函数。
异步函数不能使用任何out或ref参数。
不能在catch,finally或unsafe块中使用await操作符。
不能在await操作符之前获得一个支持线程所有权或递归的锁,并在await操作符之后释放它。在C# lock语句中使用await,编译器会报错。
在查询表达式中,await操作符只能在初始from子句的第一个集合表达式中使用,或者在join子句的集合表达式中使用。
28.3 编译器如何将异步函数转换成状态机
任何时候使用await操作符,编译器都会获取操作数,并尝试在它上面调用GetAwaiter方法。这可能是实例方法或扩展方法。调用GetAwaiter方法所返回的对象称为awaiter,正是它将被等待的对象与状态机粘合起来。
状态机获得awaiter后,会查询其IsCompleted属性。如果操作已经以同步方式完成了,属性返回true,而最为一项优化措施,状态机将继续执行并调用awaiter的GetResult方法。该方法要么抛出异常,要么返回结果。如果操作以异步方式完成,IsCompleted将返回false。状态机调用awaiter的OnCompleted方法并向它传递一个委托(引用状态机的MoveNext方法)。现在,状态机允许它的线程回到原地以执行其他代码。将来某个时候,封装了底层任务的awaiter会在完成时调用委托以执行MoveNext。可根据状态机中的字段知道如何到达代码中的正确位置,使方法能从它当初离开的位置继续。这时,代码调用awaiter的GetResult方法。执行将从这里继续,以便对结果进行处理。
28.4 异步函数扩展性
在扩展性方面,能用Task对象包装一个将来完成的操作,就可以用await操作符来等待该操作。
用一个类型(Task)来表示各种异步操作对编码有利,因为可以实现组合操作(比如Task的WhenAll和WhenAsy方法)和其他有用的操作。
除了增强使用Task时的灵活性,异步函数另一个对扩展性有利的地方在于编译器可以在await的任何操作数上调用GetAwaiter。所以操作数不一定是Task对象。可以是任意类型,只要提供了一个可以调用的GetAwatier方法。
28.5 异步函数和事件处理程序
异步函数的返回类型一般是Task或Task<TResult>,它们代表函数的状态机完成。但异步函数是可以返回void的。实现异步事件处理程序时,C#编译器允许你利用这个特殊情况简化编码。
方法签名:void EventHandlerCallback(Object sender, EventArgs e);
28.6 FCL的异步函数
异步函数很容易分辨,因为规范要求为方法名附加Async后缀。
System.IO.Stream 的所有派生类都提供了ReadAsync,WriteAsync,FlushAsync和CopyToAsync方法。
System.IO.TextReader 的所有派生类都提供了ReadAsync,ReadLineAsynchronous,ReadToEndAsync和ReadBlockAsync方法。System.IO.TextWriter的派生类提供了WriteAsync,WriteLineAsyc和FlushAsync方法。
System.Net.Http.HttpClient 类提供了GetAsync,GetSteamAsync,GetByteArrayAsync,PostAsync,PutAsynchronous,DeleteAsync和其他许多方法。
System.Net.WebRequest 的所有派生类(包括FileWebRequest,FtpWebRequest和HttpWebRequest)都提供了GetRequestStreamAsync和GetRequestAsync方法
System.Data.SqlClient.SqlCommand 类提供了ExecuteDbDataReaderAsync,ExecuteNonQueryAsync,ExecuteReaderAsync,ExecuteScalarAsync和ExecuteXmlReaderAsync方法。
生成Web服务代理类型的工具(比如SvcUtil.exe)也生成XxxAsync方法
28.7 异步函数和异常处理
Windows设备驱动程序处理异步I/O请求时可能出错:设备驱动程序会向CLR的线程池post已完成的IRP。一个线程池线程会完成Task对象并设置异常。你的状态机方法恢复时,await操作符发现操作失败并引发该异常。
如果状态机出现未处理的异常,那么代表异步函数的Task对象会因为未处理的异常而完成。然后,正在等待该Task的代码会看到异常。但异步函数也可能使用了void返回类型,这是调用者就没办法发现未处理的异常。所以,但返回void的异步函数抛出未处理的异常时,编译器生成的代码将捕捉它,并使用调用者的同步上下文重新抛出它。如果调用者通过GUI线程执行,GUI线程最终将重新抛出异常。重新抛出这个异常通常造成整个进程终止。
28.8 异步函数的其他功能
VS为异步函数提供了出色的支持。
C#异步lambda表达式。
28.9 应用程序及其线程处理模型
.Net Framework支持几种不同的应用程序模型,而每种模型都可能引入了它自己的线程处理模型。控制台应用程序和Windows服务(实际也是控制台应用程序:只是看不见控制台而已)没有引入任何线程才处理模型;换言之,任何线程可在任何时候做它想做的任何事情。
但GUI应用程序(包括Windows窗体,WPF,Silverlight和Windows Store应用程序)引入了一个线程处理模型。在这个模型中,UI元素只能由创建它的线程更新。
ASP.NET应用程序允许任何线程做它想做的任何事情。线程池线程开始处理一个客户端的请求时,可以对客户端的语言文化做出假定,从而允许Web服务器对返回的数字,日期和时间进行该语言文化特有的格式化处理。此外,Web服务器还可对客户端的身份标识做出假定,确保只能访问客户端有权访问的资源。
28.10 以异步方式实现服务器
要构建异步ASP.NET Web窗体,在.aspx文件中添加Async=“true”网页指令,并参考System.Web.UI.Page的RegisterAsyncTask方法。
要构建异步ASP.NET MVC控制器,使你的控制器类从System.Web.Mvc.AsyncController派生,让操作方法返回一个Task<ActionResult>即可。
要构建异步ASP.NET处理程序,使你的类从System.Web.HttpTaskAsyncHandler派生,重写其抽象ProcessRequestAsync方法
要构建异步WCF服务,将服务作为异步函数实现,让它返回Task或Task<TResult>。
28.11 取消I/O操作
实现一个WithCancellation方法来扩展Task<TResult>
28.12 有的I/O操作必须同步进行
FCL不能以异步方式打开文件,访问注册表,访问事件日志,获取目录的文件/子目录或者更改文件/目录的属性等
Windows Runtime允许以异步方法执行I/O操作。可以使用C#的异步函数功能简化调用这些API时的编码。
28.13 I/O请求优先级
Windows允许线程在发出I/O请求时指定优先级,FCL不包含此功能,可采取P/Invoke本机Win32函数的方式。
要调用ThreadIO的BeginBackgrouondProcessing方法,告诉Windows你的线程要发出低优先级I/O请求。这同时会降低线程的CPU调度优先级。可调用EndBackgroupProcessing,或者在BeginBackgroundProcessing返回的值上调用Dispose,使线程恢复为发出普通优先级的I/O请求(以普通CPU调度优先级)。线程只能影响它自己的后台处理模式;Windows不允许线程更改一个线程的后台处理模式。
如果希望一个进程中的所有线程发出低优先级I/O请求和进行低优先级的CPU调度,可调用BeginBackgroundProcessing,为它的Process参数传递true值,一个进程只能影响它自己的后台处理模式;Windows不允许一个线程更改另一个进程的后台处理模式。