C#异步编程笔记

0x00异步编程模式的历史.NETFramework提供了执行异步操作的三种模式:异步编程模型(APM)模式(即IAsyncResult模式),在该模式下,异步操作需要使用Begin和End方法(例如,异步写入操作需要使用BeginWrite和EndWrite方法)不建议新的开发使用此模式。有关详细信息,请参阅异步编程模型(APM)。基于事件的异步模式(EAP),这种模式需要Async后缀,也需要...

C#异步编程笔记

0x00 异步编程模式的历史

.NET Framework 提供了执行异步操作的三种模式:

  • 异步编程模型 (APM)模式(即IAsyncResult模式),在该模式下,异步操作需要使用BeginEnd方法(例如,异步写入操作需要使用BeginWriteEndWrite方法)不建议新的开发使用此模式。有关详细信息,请参阅异步编程模型 (APM)。

  • 基于事件的异步模式 (EAP),这种模式需要Async后缀,也需要一个或多个事件、事件处理程序委托类型和EventArg派生类型。EAP 是在 .NET Framework 2.0 中引入的。不建议新的开发使用这种模式。有关详细信息,请参阅基于事件的异步模式 (EAP)。

  • 基于任务的异步模式 (TAP),该模式使用单一方法表示异步操作的开始和完成。TAP 是在 .NET Framework 4 中引入的,并且它是在 .NET Framework 中进行异步编程的推荐使用方法。C# 中的async和await关键词以及 Visual Basic 语言中的Async和Await运算符为 TAP 添加了语言支持。有关详细信息,请参阅基于任务的异步模式 (TAP)。

现在主要使用TAP来编程。

0x01 Task和 Task<T>

任务是用于实现称之为并发 Promise 模型的构造。简单地说,它们“承诺”,会在稍后完成工作,让你使用干净的 API 与 promise 协作。

  • Task表示不返回值的单个操作。
  • Task<T>表示返回T类型的值的单个操作。

请务必将任务理解为工作的异步抽象,而非在线程之上的抽象。默认情况下,任务在当前线程上执行,且在适当时会将工作委托给操作系统。可选择性地通过Task.RunAPI 显式请求任务在独立线程上运行。

任务会公开一个 API 协议来监视、等候和访问任务的结果值(如Task<T>)。含有await关键字的语言集成可提供高级别抽象来使用任务。

任务运行时,使用await在任务完成前将控制让步于其调用方,可让应用程序和服务执行有用工作。任务完成后代码无需依靠回调或事件便可继续执行。语言和任务 API 集成会为你完成此操作。如果正在使用Task<T>,任务完成时,await关键字还将“打开”返回的值。下面进一步详细介绍了此工作原理。

0x02 针对 I/O 的操作的Task

以下部分介绍了使用典型异步 I/O 调用时会出现的各种情况。我们先看两个例子。

第一个示例调用异步方法,并返回活动任务,很可能尚未完成。

C#
public Task<string> GetHtmlAsync(){ // Execution is synchronous here var client = new HttpClient(); return client.GetStringAsync("http://www.dotnetfoundation.org");}

第二个示例还使用了asyncawait关键字对任务进行操作。

C#
public async Task<string> GetFirstCharactersCountAsync(string url, int count){ // Execution is synchronous here var client = new HttpClient(); // Execution of GetFirstCharactersCountAsync() is yielded to the caller here // GetStringAsync returns a Task<string>, which is *awaited* var page = await client.GetStringAsync("http://www.dotnetfoundation.org"); // Execution resumes when the client.GetStringAsync task completes, // becoming synchronous again. if (count > page.Length) {  return page; } else {  return page.Substring(0, count); }}

GetStringAsync()的调用通过低级别 .NET 库进行(可能是调用其他异步方法),直到其到达 P/Invoke 互操作调用,进入本机网络库。本机库随后可能会调入系统 API 调用(例如 Linux 上套接字的write())。可能会使用TaskCompletionSource在本机/托管边界创建一个任务对象。将通过层向上传递任务对象,对其进行操作或直接返回,最后返回到初始调用方。

在上述的第二个示例中,Task<T>对象将直接从GetStringAsync返回。由于使用了await关键字,因此该方法会返回一个新建的任务对象。控制会从GetFirstCharactersCountAsync方法中的该位置返回到调用方。Task<T>对象的方法和属性确保调用方监视任务进度,当执行完 GetFirstCharactersCountAsync 中剩余的代码时,任务便完成。

调用系统 API 后,请求位于内核空间,一路来到操作系统的网络子系统(例如 Linux 内核中的/net)。此处操作系统将对网络请求进行异步处理。所用操作系统不同,细节可能有所不同(可能会将设备驱动程序调用安排为发送回运行时的信号,或者会执行设备驱动程序调用然后有一个信号发送回来),但最终都会通知运行时网络请求正在进行中。此时,设备驱动程序工作处于已计划、正在进行或是已完成(请求已“通过网络”发出),但由于这些均为异步进行,设备驱动程序可立即着手处理其他事项!

例如,在 Windows 中操作系统线程调用网络设备驱动程序并要求它通过表示操作的中断请求数据包 (IRP) 执行网络操作。设备驱动程序接收 IRP,调用网络,将 IRP 标记为“待定”,并返回到操作系统。由于现在操作系统线程了解到 IRP 为“待定”,因此无需再为此作业进行进一步操作,将其“返回”,这样它就可用于完成其他工作。

请求完成且数据通过设备驱动程序返回后,会经由中断通知 CPU 新接收到的数据。处理中断的方式因操作系统不同而有所不同,但最终都会通过操作系统将数据传递到系统互操作调用(例如,Linux 中的中断处理程序将安排 IRQ 的下半部分通过操作系统异步向上传递数据)。请注意这仍是异步进行的!在下一个可用线程能执行异步方法且“打开”已完成任务的结果前,结果会排队等候。

在整个过程中,关键点在于没有线程专用于运行任务。尽管需要在一些上下文中执行工作(即,操作系统确实必须将数据传递到设备驱动程序并响应中断),但没有专用于等待数据从请求返回的线程。这让系统能处理更多的工作而不是等待某些 I/O 调用结束。

这对服务器方案而言意味着什么?

此模型可很好地处理典型的服务器方案工作负荷。由于没有专用于阻止未完成任务的线程,服务器线程池可服务更多的 Web 请求。相比服务器将线程专用于接收到的每个请求,使用asyncawait能够使服务器多处理一个数量级的请求。

这对客户端方案而言意味着什么?

源文地址:https://www.guoxiongfei.cn/cntech/1959.html
0