·您现在的位置: 江北区云翼计算机软件开发服务部 >> 文章中心 >> 网站建设 >> 网站建设开发 >> ASP.NET网站开发 >> ASP.NET 上的 Async/Await 简介

ASP.NET 上的 Async/Await 简介

作者:佚名      ASP.NET网站开发编辑:admin      更新时间:2022-07-23

asp.net 上的 Async/Await 简介

原文链接

大多数有关 async/await 的在线资源假定您正在开发客户端应用程序,但在服务器上有 async 的位置吗?可以非常肯定地回答“有”。本文是对 ASP.NET 上异步请求的概念性概述,并提供了对最佳在线资源的引用。我不打算介绍 async 或 await 的语法;因为我已经在一篇介绍性的博客文章 (bit.ly/19IkogW) 以及一篇关于 async 最佳做法的文章 (msdn.microsoft.com/magazine/jj991977) 中介绍过了。本文将特别重点介绍 async 在 ASP.NET 上的工作原理。

对于客户端应用程序,如 Windows 应用商店、Windows 桌面和 Windows Phone 应用程序,async 的主要优点是出色的响应能力。这些类型的应用程序使用 async 主要是为了保证用户界面的响应能力。对于服务器应用程序,async 异步的主要优点是不错的可扩展性。Node.js 可扩展性的关键是其固有的异步本质;Open Web Interface for .NET (OWIN) 针对异步进行了全新设计;ASP.NET 也可以是异步的。Async:不仅仅适用于 UI 应用程序!

同步与异步请求处理

在深入探讨异步请求处理程序之前,我想简要地回顾同步请求处理程序在 ASP.NET 上的工作原理。在本例中,假设系统中的请求依赖于一些外部资源,如数据库或 Web API。当收到请求时,ASP.NET 将其中的一个线程池线程分配给该请求。因为它是同步编写,所以请求处理程序将同步调用该外部资源。这将阻止请求线程,直到返回对外部资源的调用。图 1说明了具有两个线程的线程池,其中有一个线程被阻止,正在等待外部资源。

同步等待外部资源图 1 同步等待外部资源

最后,返回该外部资源的调用,并且请求线程恢复处理该请求。当完成该请求,且准备好发送响应时,请求线程将返回到线程池中。

这一切好倒是好,但是您的 ASP.NET 服务器获得的请求总会超出它的线程能够处理的数量。这时候,额外的请求必须等到有线程可用时才能够运行。图 2说明的仍是该双线程服务器,此时它收到三个请求。

收到三个请求的双线程服务器图 2 收到三个请求的双线程服务器

在这种情况下,前两个请求都被分配到线程池中的线程。每个请求都调用外部资源,于是阻止了它们的线程。第三个请求必须等待有线程可用时,才可以开始进行处理,但该请求已经在系统中。它的计时器一直在工作,它正处于发生 HTTP 错误 503(服务不可用)的危险之中。

但是对此考虑一下:这第三个请求正在等待可用线程,与此同时系统中的另外两个线程实际上什么都没做。这些线程受到阻止,都在等待返回外部调用。它们确实没有做任何实际工作;它们不处于运行状态,也不占用任何 CPU 时间。这些线程被白白浪费掉,但还有请求处于需要中。以下是异步请求解决的情况。

异步请求处理程序的操作方式与此不同。当收到请求时,ASP.NET 将其中的一个线程池线程分配给该请求。这一次,请求处理程序将异步调用该外部资源。当返回对外部资源的调用之前,已将此请求线程返回到线程池中。图 3说明当请求在异步等待外部资源时具有两个线程的线程池。

异步等待外部资源图 3 异步等待外部资源

重要的区别在于,在进行异步调用的过程中,已将请求线程返回到线程池中。当线程处于线程池中时,它不再与该请求相关联。此时,当返回外部资源调用时,ASP.NET 将其线程池中的一个线程重新分配给该请求。该线程将继续处理该请求。当请求完成时,该线程再次返回到线程池中。注意,对于同步处理程序,同一线程会用于该请求的整个生命周期;相反,对于异步处理程序,可以将不同的线程分配给同一请求(在不同的时间)。

现在,如果三个请求都进来,服务器就可以轻松处理了。因为每当请求在等待异步工作时,线程就会被释放到线程池中,它们可以自由处理新的以及现有的请求。异步请求可以让数量较少的线程去处理数量较多的请求。因此,ASP.NET 上的异步代码的主要优点是出色的可扩展性。

为什么不增加线程池的大小?

此时,总是会被问到:为什么不增加线程池的大小?答案有两个:与阻止线程池线程相比,异步代码扩展得更深层且更快。

异步代码的扩展性超过阻止线程,这是因为它使用的内存更少;在现代操作系统上每个线程池线程具有 1MB 的堆栈,外加一个不分页的内核堆栈。这听起来好像很多,但当您的服务器上有一大堆线程时,会发现其实不够用。与此相反,异步操作的内存开销要小得多。因此,使用异步操作的请求比使用阻止线程的请求面临更少的内存压力。异步代码使您可以将更多的内存用于其他任务(例如缓存)。

异步代码在速度上比阻止线程更快,因为线程池的注入速度有限。截至发稿时,该速度为每两秒钟一个线程。注入速度有限是件好事;它避免了持续的线程构建和破坏。然而,考虑一下请求蜂拥而至时会发生什么。同步代码很容易就会陷入瘫痪,因为请求将用光所有可用的线程,其余请求必须等待线程池有新的线程注入。而另一方面,异步代码不需要有这样的限制;它是“始终开放”的,可以这么说。异步代码能够更出色地响应请求量突然波动。

请记住,异步代码不会取代线程池。不应该只有线程池或异步代码;而是要同时拥有线程池和异步代码。异步代码可以让您的应用程序充分利用线程池。它使用现有的线程池,并把它提高到 11。

线程执行异步工作怎么样?

我一直被人问到这个问题。这意味着,必须有一些线程阻止对外部资源进行 I/O 调用。因此,异步代码释放请求线程,但这只能以牺牲系统中另一个线程为代价吧?没有,一点关系也没有。

要了解异步请求为什么扩展,我将跟踪一个异步 I/O 调用的(简化)示例。假设有一个请求需要写入到文件中。请求线程调用异步写入方法。WriteAsync 由基类库 (BCL) 实现,并使用其异步 I/O 的完成端口。因此,WriteAsync 调用会作为异步文件写入传递到 OS 中。然后,OS 与驱动程序堆栈进行通信,同时传递数据以写入到 I/O 请求数据包 (IRP) 中。

现在,有趣的事情发生了:如果设备驱动程序不能立即处理 IRP,就必须异步进行处理。因此,驱动程序告诉磁盘开始写入,并将“挂起”响应返回到 OS 中。OS 将“挂起”响应传递到 BCL,然后 BCL 将一个不完整的任务返回到请求处理代码。请求处理代码等待将不完整的任务从该方法等处返回的任务。最后,请求处理代码最终向 ASP.NET 返回一个不完整的任务,并且请求线程被释放回线程池中。

现在,考虑系统的当前状态。已经分配了各种 I/O 结构(例如,任务实例和 IRP),而且它们都处在挂起/不完整的状态。然而,没有任何线程因等待写入操作完成而受到阻止。ASP.NET、BCL、OS 以及设备驱动程序都没有专门用于异步工作的线程。

当磁盘完成写入数据时,它通过中断通知其驱动程序。该驱动程序会通知 OS 该 IRP 已经完成,并且 OS 会通过完成端口通知 BCL。线程池线程通过完成从 WriteAsync 返回的任务来响应该通知;这反过来又恢复异步请求代码。在该完成通知阶段中,短期“借用”了一些线程,但实际上没有线程在写入过程中受到阻止。

本示例经过极大地简化,但是要点突出:真正的异步工作并不需要线程。实际推送字节也无需占用 CPU 时间。还有一个辅助课程要了解。考虑一下在设备驱动程序的世界里,设备驱动程序如何做才能立即或异步处理 IRP。同步处理是不是一个选项。在设备驱动程序级别,所有重要的 I/O 都是异步的。许多开发人员的思维模式都是把用于 I/O 操作的“普通 API”认为是同步的,异步 API 作为一层建立在普通的同步 API 上。然而,这恰恰相反:实际上,普通 API是​​异步的;使用异步 I/O 实现的是正是同步 API!

为什么没有了异步处理程序?

如果异步请求处理是如此完美,那它为什么还不可用?事实上,异步代码非常适合扩展,因此从 Microsoft .NET Framework 形成之初到现在,ASP.NET 平台一直支持异步处理程序和模块。ASP.NET 2.0 引入了异步网页,ASP.NET MVC 2 中 MVC 得到了异步控制器。

然而,最近,异步代码在编写上总是有些问题,并且难于维护。许多公司便决定同步开发代码、支付更大的服务器场或更昂贵的托管,这样就会简单一些。现在,出现了逆转:在 ASP.NET 4.5 中,使用 async 和 await 的异步代码几乎与编写同步代码一样简单。由于大型系统迁移到云托管并要求更加有规模,越来越多的公司开始青睐 ASP.NET 上的 async 和 await。

异步代码不是灵丹妙药

异步请求处理尽管很强大,但它不会解决所有问题。关于 ASP.NET 上的 async 和 await 可以做什么的问题,存在一些常见的误解。

当一些开发人员了解 async 和 await 后,他们认为这是服务器代码“让步”于客户端(例如浏览器)的一种方式。然而,ASP.NET 上的 async 和 await 只“让步”于 ASP.NET 运行时;HTTP 协议保持不变,您对每个请求仍只有一个响应。如果在 async/await 之前您需要 SignalR 或 Ajax 或 UpdatePanel,那么在 async/await 之后仍然需要 SignalR 或 AJAX 或 UpdatePanel。

使用 async 和 await 的异步请求处理可以帮助扩大您的应用程序规模。然而,这是在一台服务器上的扩展;您可能仍然需要对扩展进行规划。如果您确实需要扩展体系结构,将仍然需要考虑无状态的幂等请求和可靠的队列。Async/await 多少有所帮助:它们使您能够充分利用服务器资源,所以您不需要经常进行扩展。但是,如果您确实需要向外扩展,您将需要一个合适的分布式体系结构。

ASP.NET 上的 async 和 await 都是关于 I/O 的。它们非常适合读取和写入文件、数据库记录和 REST API。然而,它们不能很好地执行占用大量 CPU 的任务。您可以通过等待 Task.Run 开始一些背景工作,但这样做没有任何意义。事实上,通过启发式干扰 ASP.NET 线程池会损害您的可扩展性。如果您要在 ASP.NET 上执行占用大量 CPU 的工作,最好的办法是直接在请求线程上执行该工作。通常,不要将工作排队送到 ASP.NET 上的线程池。

最后,在整体上考虑系统的可扩展性。十年前,常见的体系结构要有一个可与后端的 SQL Server 数据库进行通信的 ASP.NET Web 服务器。在这种简单的体系结构中,通常数据库服务器是可扩展性的瓶颈,而不是 Web 服务器。让您的数据库调用异步可能起不到帮助作用;当然您可以用它们来扩展 Web 服务器,但数据库服务器将阻止整个系统的扩展。

Rick Anderson 在他精彩的博客文章中针对异步数据库调用给出案例,“我的数据库调用应该是异步的吗?”(bit.ly/1rw66UB)。以下是两点支持论据:首先,异步代码有难度(因而开发人员的时间成本比只是购买较大的服务器要高);其次,如果数据库后端是瓶颈,那么扩展 Web 服务器没有什么意义。在写这篇文章时,这两方面的论据非常有道理,但随着时间的推移这两个论据的意义已经慢慢弱化。首先,使用 async 和 await 编写异步代码更加容易了。其次,随着全球逐步采用云计算,网站的数据后端逐渐得到扩展。诸如 Microsoft Azure SQL 数据库、NoSQL 以及其他 API 之类的现代后端与单个 SQL Server 相比可以得到更进一步的扩展,从而将瓶颈又推回 Web 服务器。在这种情况下,async/await 可以通过扩展 ASP.NET 带来巨大的优势。

在开始之前

首先您需要知道只有 ASP.NET 4.5 支持 async 和 await。有一个叫做 Microsoft.Bcl.Async 的 NuGet 程序包可为 .NET Framework 4 启用 async 和 await,但并不使用它;这将无法正常工作!其原因是,为了能与 async 和 await 更好地一起工作,ASP.NET 本身必须更改其管理异步请求处理的方式;NuGet 程序包中包含编译器需要的所有类型,但不会修补 ASP.NET 运行时。没有解决方法;您需要 ASP.NET 4.5 或更高版本。

接下来,要知道,ASP.NET 4.5 在服务器上引入了“quirks 模式”。如果您创建一个新的 ASP.NET 4.5 项目,则不必担心。但是,如果要将现有的项目升级到 ASP.NET 4.5,所有 quirk 都将被打开。我建议您​​通过编辑 web.config 并将 httPRuntime.targetFramework 设置为 4.5 把它们全部关闭。如果使用此设置的应用程序失败(并且您不想花时间去修复它),至少您可以通过为 aspnet:UseTaskFriendlySynchronizationContext 的 appSetting 键添加值“true”来获取 async/await 工作。如果您将 httpRuntime.targetFramework 设置为 4.5,则 appSetting 键不必要。Web 开发团队已在bit.ly/1pbmnzK发表一篇关于这一新的“quirks 模式”的详细信息的博客。提示: 如果您看到出现奇怪的行为或例外情况,并且您的调用堆栈包括 LegacyAspNetSynchronizationContext,那么您的应用程序正在这个“quirks 模式”下运行。LegacyAspNetSynchronizationContext 与异步不兼容;您在 ASP.NET 4.5 上需要常规的 AspNetSynchronizationContext。

在 ASP.NET 4.5 中,所有的 ASP.NET 设置都针对异步请求设置了很好的默认值,但也有几个其他设置您可能要更改。首先是 IIS 设置:考虑将 IIS/HTTP.sys 的队列限制(应用程序池|高级设置|队列长度)从默认的 1,000 提高到 5,000。另一个是 .NET 运行时设置:ServicePointManager.DefaultConnectionLimit,它的默认值是内核数量的 12 倍。DefaultConnectionLimit 限制到同一主机名的传出连接数。

关于中止请求的提示

当 ASP.NET 同步处理一个请求时,它有一个非常简单的机制可以中止请求(例如,如果请求超出其超时值):它会中止该请求的工作线程。这是有道理的,因为在同步领域,每个请求从开始到结束都使用同一个工作线程。中止线程对于 AppDomain 的长期稳定性而言尚不完美,因此默认情况下 ASP.NET 将定期回收您的应用程序,以保持干净。

对于异步请求,如果要中止请求,ASP.NET 并不会中止工作线程。相反,它会取消使用 CancellationToken 的请求。异步请求处理程序应该接受并遵守取消标记。大多数较新的框架(包括 Web API、MVC 和 SignalR)将构建并直接向您传递 CancellationToken;您需要做的就是把它声明为一个参数。您也可以直接访问 ASP.NET 标记;例如,HttpRequest.TimedOutToken 是当请求超时时取消的一个 CancellationToken。

随着应用程序迁移到云,中止请求就显得更为重要。基于云的应用程序也越来越依赖于可能占用任意时间量的外部服务。例如,一种标准模式是使用指数回退来重试外部请求;如果您的应用程序依赖于类似这样的多种服务,对您的请求处理在整体上应用一个超时上限不失为一个好方法。

Async 支持的现状

针对 async 的兼容性问题,已对许多库进行了更新。在版本 6 中已将 async 支持添加到实体框架(在 EntityFramework NuGet 程序包中)。不过,当以异步方式运行时,您必须要小心操作以避免延迟加载,因为延迟加载总是以同步方式执行。HttpClient(在 Microsoft.Net.Http NuGet 程序包中)是采用 async 理念设计而成的现代 HTTP 客户端,是调用外部 REST API 的理想选择;是 HttpWebRequest 和 WebClient 的现代版替代品。在 2.1 版本中,Microsoft Azure 存储客户端库(在 WindowsAzure.Storage NuGet 程序包中)添加了异步支持。

较新的框架(如 Web API 和 SignalR)对 async 和 await 提供全面的支持。个别 Web API 已围绕 async 支持建立起整个管道:不仅有异步控制器,还有异步筛选器和处理程序。Web API 和 SignalR 有一个很平凡的异步故事:您可以“放手去做”然后“就会成功”。

这给我们带来了一个令人伤感的故事:如今,ASP.NET MVC 只是部分支持 async 和 await。有基本的支持——异步控制器的操作和取消工作正常。ASP.NET 网站上有关于如何使用 ASP.NET MVC 中的异步控制器操作的精彩教程 (bit.ly/1m1LXTx);这对于 MVC 上的 async 入门是绝佳的资源。不幸的是