《CLR via C#》笔记:第5部分 线程处理(2)

  1.7 C#, 3.3 大数据处理
  • 本博客所总结书籍为《CLR via C#(第4版)》清华大学出版社,2021年11月第11次印刷(如果是旧版书籍或者pdf可能会出现书页对不上的情况)
  • 你可以理解为本博客为该书的精简子集,给正在学习中的人提供一个“glance”,以及对于部分专业术语或知识点给出解释/博客链接。
  • 【本博客有如下定义“Px x”,第一个代表书中的页数,第二个代表大致内容从本页第几段开始。(如果有last+x代表倒数第几段,last代表最后一段)】
  • 电子书可以在博客首页的文档-资源归档中找到,或者点击:传送门自行查找。如有能力请支持正版。(很推荐放在竖屏上阅读本电子书,这多是一件美事)
  • 欢迎加群学习交流:637959304 进群密码:(CSGO的拆包密码) 

  • 本人对于线程的经验仅限于开辟线程以及线程同步方面的简易操作。对于深入工程应用实践仍有缺乏,故本大部分的笔记补充能容可能会在未来半年-一年内陆续进行。

第二十八章 I/O限制的异步操作

Windows如何执行I/O操作

  • 程序通过构造一个FileStream对象来打开磁盘文件,然后调用Read方法从文件中读取数据。调用FileStream 的 Read方法时,你的线程从托管代码转变为本机/用户模式代码, Read内部调用Win32 ReadFile 函数(①)。ReadFile分配一个小的数据结构,称为I/O请求包(I/ORequest Packet,IRP)(②)。IRP结构初始化后包含的内容有:文件句柄,文件中的偏移量(从这个位置开始读取字节),一个 Byte[]|数组的地址(数组用读取的字节来填充),要传输的字节数以及其他常规性内容。
  • 然后,ReadFile 将你的线程从本机/用户模式代码转变成本机/内核模式代码,向内核传递IRP数据结构,从而调用Windows内核(③)。根据IRP中的设备句柄,Windows 内核知道I/O操作要传送给哪个硬件设备。因此,Windows将IRP传送给恰当的设备驱动程序的IRP队列(④)。每个设备驱动程序都维护着自己的IRP队列,其中包含了机器上运行的所有进程发出的IO请求。IRP数据包到达时,设备驱动程序将IRP信息传给物理硬件设备上安装的电路板。现在,硬件设备将执行请求的1/O操作(⑤)。
  • 最终,硬件设备会完成IO操作。然后,Windows 会唤醒你的线程,把它调度给一个CPU,使它从内核模式返回用户模式,再返回至托管代码(⑦,⑧和⑨)。FileStream 的Read方法现在返回一个Int32,指明从文件中读取的实际字节数,使你知道在传给Read的 Byte[]中,实际能检索到多少个字节。(P644 last2)
image 83 - 《CLR via C#》笔记:第5部分 线程处理(2)
Windows如何执行异步操作
  • 下图删除了除硬盘之外的所有硬件设备,引入了CLR的线程池,稍微修改了代码。打开磁盘文件的方式仍然是通过构造一个FileStream对象,但现在传递了一个FileOptions.Asynchronous标志,告诉Windows我希望文件的读/写操作以异步方式执行。
  • 现在调用ReadAsync而不是 Read从文件中读取数据。ReadAsync内部分配一个Task对象来代表用于完成读取操作的代码。然后,ReadAsync调用Win32 ReadFile函数(①)。ReadFile分配 IRP,和前面的同步操作一样初始化它(②),然后把它传给Windows内核(③)。Windows把 IRP添加到硬盘驱动程序的IRP队列中(④)。但线程不再阻塞,而是允许返回至你的代码。所以,线程能立即从 ReadAsync调用中返回(⑤,⑥和⑦)。当然,此时IRP可能尚未处理好,所以不能够在ReadAsync之后的代码中访问传递的Byte[]中的字节。(P645 last2)
image 84 - 《CLR via C#》笔记:第5部分 线程处理(2)
引入CLR

C#的异步函数

  • 执行异步操作是构建可伸缩的、响应灵敏的应用程序的关键,它允许使用少量线程执行大量操作。与线程池结合,异步操作允许利用机器中的所有CPU。(P647 last)
  • Async关键字,异步函数操作代码:(P648)
  • 异步函数存在的限制:
    1、不能将应用程序的Main方法转变成异步函数。另外,构造器、属性访问器方法和事件访问器方法不能转变成异步函数。
    2、异步函数不能使用任何out或ref参数。
    3、不能在 catch,finally 或unsafe块中使用await操作符。
    4、不能在 await操作符之前获得一个支持线程所有权或递归的锁,并在 await操作符之后释放它。这是因为 await之前的代码由一个线程执行,之后的代码则可能由另一个线程执行。在C# lock 语句中使用await,编译器会报错。如果显式调用 Monitor 的Enter和 Exit方法,那么代码虽然能编译,但Monitor.Exit会在运行时抛出一个SynchronizationLockException。
    4、在查询表达式中,await操作符只能在初始from子句的第一个集合表达式中使用,或者在join子句的集合表达式中使用。

编译器如何将异步函数转换成状态机

  • 不要让线程等待一个线程同步构造从而造成线程的阻塞。相反,可以等待(await)从 SemaphoreSlim的WaitAsync方法或者我自己的OneManyLock 的AcquireAsync方法所返回的任务,从而避免线程被阻塞。(P649 注解)
  • 线程调用线程代码示例以及IL逆向工程后的结构。(P650-P652)

异步函数扩展性

  • 在扩展性方面,能用Task对象包装一个将来完成的操作,就可以用await操作符来等待该操作。用一个类型(Task)来表示各种异步操作对编码有利,因为可以实现组合操作(比如Task 的 WhenAll和WhenAny方法)和其他有用的操作。(P653 3)
  • 除了增强使用Task 时的灵活性,异步函数另一个对扩展性有利的地方在于编译器可以在await的任何操作数上调用GetAwaiter。所以操作数不一定是Task对象。可以是任意类型,只要提供了一个可以调用的 GetAwaiter方法。(P654 last)

异步函数和事件处理程序

  • 异步函数的返回类型一般是 Task 或Task,它们代表函数的状态机完成。但异步函数是可以返回void 的。实现异步事件处理程序时,C#编译器允许你利用这个特殊情况简化编码。几乎所有事件处理程序都遵循以下方法签名:
void EventHandlerCallback(Object sender,EventArgs e) ;

FCL的异步函数

  • 举例:(P656 2)
  • System.IO.Stream 的所有派生类都提供了ReadAsync,WriteAsync,FlushAsync和CopyToAsync方法。
  • System.IO.TextReader的所有派生类都提供了ReadAsync,ReadLineAsync,ReadToEndAsync和 ReadBlockAsync方法。System.IO.TextWriter的派生类提供了WriteAsync,WriteLineAsync和 FlushAsync方法。
  • System.Net.Http.HttpClient类提供了GetAsync,GetStreamAsync,GetByteArrayAsync,PostAsync,PutAsync,DeleteAsync和其他许多方法。
  • System.Net.WebRequest的所有派生类(包括 FileWebRequest,FtpWebRequest和HttpWebRequest)都提供了GetRequestStreamAsync和l GetResponseAsync方法。System.Data.SqlClient.SqlCommand类提供了 ExecuteDbDataReaderAsync ,ExecuteNonQueryAsync,ExecuteReaderAsync,ExecuteScalarAsync和l ExecuteXmlReaderAsync方法。
  • 生成Web服务代理类型的工具(比如SvcUtil.exe)也生成XxxAsync方法。

异步函数和异常处理

  • 如果状态机出现未处理的异常,那么代表异步函数的Task对象会因为未处理的异常而完成。然后,正在等待该Task 的代码会看到异常。但异步函数也可能使用了void返回类型,这时调用者就没有办法发现未处理的异常。所以,当返回void 的异步函数抛出未处理的异常时,编译器生成的代码将捕捉它,并使用调用者的同步上下文(稍后讨论)重新抛出它。如果调用者通过GUI线程执行,GUI线程最终将重新抛出异常。重新抛出这种异常通常造成整个进程终止。(P658 3)

异步函数的其他功能

  • 对异步函数使用逐过程调试,如果调试器在await操作符上停止,逐过程会在异步操作完成后,在抵达下一个语句时重新由调试器接管。这时执行代码的线程可能已经不是发起异步操作的线程。
    如果不小心使用逐语句操作,也可以用Shift+F11跳出。(P658 last2)

应用程序及其线程处理模型

  • .NET Framework支持几种不同的应用程序模型,而每种模型都可能引入了它自己的线程处理模型。控制台应用程序和 Windows服务(实际也是控制台应用程序;只是看不见控制台而已)没有引入任何线程处理模型;换言之,任何线程可在任何时候做它想做的任何事情。(P661 2)

以异步方式实现服务器

  • MSDN文档部分举例:
    1、要构建异步ASP.NET Web窗体,在.aspx文件中添加Async=”true”网页指令,并参考System.Web.UI.Page 的 RegisterAsyncTask方法。
    2、要构建异步ASP.NET MVC 控制器,使你的控制器类从System.Web.Mvc.AsyncController派生,让操作方法返回一个 Task即可。
    3、要构建异步 ASP.NET处理程序,使你的类从System.web.HttpTaskAsyncHandler派生,重写其抽象ProcessRequestAsync方法。
    4、要构建异步 WCF服务v将服务作为异步函数实现,让它返回Task或Task。

取消I/O操作

  • 建议实现一个 WithCancellation扩展方法来扩展Task(需要类似的重载版本来扩展Task),代码(P664 last)

有的I/O操作必须同步进行

  • 考虑到同步IO操作的各种问题,在设计Windows Runtime的时候,Windows团队决定公开以异步方式执行I/O的所有方法。所以,现在可以用一个Windows Runtime APIl来异步地打开文件了,详情参见 Windows.Storage.StorageFile 的OpenAsync方法。事实上,Windows Runtime没有提供以同步方式执行IO操作的任何API。幸好,可以使用C#的异步函数功能简化调用这些API时的编码。(P665 last2)
  • FileStream在异步通信方面会有一些特有问题。(P665 last)

I/O请求优先级

  • 低优先级线程可能会挂起高优先级线程,导致后者不能快速完成工作。(P666 last2)
  • 可以使用Thread的BeginBackgroundProcessing方法改变你的线程优先级I/O请求。Windows不允许线程更改另一个线程的后台处理模式(P667 last)

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=nocxzncdfnu9

LEAVE A COMMENT