在 ASP.NET MVC 4 中使用异步方法

作者 :Rick Anderson

本教程将介绍如何使用 Visual Studio Express 2012 for Web(Microsoft Visual Studio 的免费版本)生成异步 ASP.NET MVC Web 应用程序。 也可以使用 Visual Studio 2012

github 上的本教程提供了完整示例 https://github.com/RickAndMSFT/Async-ASP.NET/

ASP.NET MVC 4 Controller 类组合 使用 .NET 4.5 ,可以编写返回 Task<ActionResult> 类型的对象的异步操作方法。 .NET Framework 4 引入了称为 Task 的异步编程概念,ASP.NET MVC 4 支持 Task。 任务由 System.Threading.Tasks 命名空间中的 Task 类型和相关类型表示。 .NET Framework 4.5 基于此异步支持构建,其中包含 awaitasync 关键字,使使用 Task 对象比以前的异步方法要复杂得多。 await 关键字 (keyword) 是语法简写,用于指示一段代码应异步等待其他一段代码。 异步关键字 (keyword) 表示可用于将方法标记为基于任务的异步方法的提示。 awaitasyncTask 对象的组合使你在 .NET 4.5 中编写异步代码变得更加容易。 异步方法的新模型称为 基于任务的异步模式 (TAP) 。 本教程假设你熟悉使用 awaitasync 关键字以及 Task 命名空间的异步编程。

有关使用 awaitasync 关键字以及 Task 命名空间的详细信息,请参阅以下参考。

线程池处理请求的方式

在 Web 服务器上,.NET Framework维护用于为 ASP.NET 请求提供服务的线程池。 当请求到达时,将调度池中的线程以处理该请求。 如果同步处理请求,则处理请求的线程在处理请求时处于繁忙状态,并且该线程无法为另一个请求提供服务。

这可能不是问题,因为线程池可以变得足够大,可以容纳许多繁忙的线程。 但是,线程池中的线程数受到限制, (.NET 4.5 的默认最大值为 5,000) 。 在长时间运行的请求具有高并发性的大型应用程序中,所有可用线程都可能正忙。 这种情况称为“线程不足”。 达到此条件时,Web 服务器会将请求排队。 如果请求队列已满,Web 服务器会拒绝 HTTP 503 状态的请求, (服务器太忙) 。 CLR 线程池对新线程注入有限制。 如果并发是突发 (也就是说,网站可能会突然收到大量请求) 并且所有可用请求线程由于后端调用而忙于高延迟,那么有限的线程注入速率会使应用程序响应非常差。 此外,添加到线程池的每个新线程都有开销 (,例如 1 MB 的堆栈内存) 。 使用同步方法为高延迟调用提供服务的 Web 应用程序(其中线程池增长到 .NET 4.5 默认的最大线程数为 5,000 个线程)会比能够使用异步方法和仅 50 个线程服务相同请求的应用程序多消耗大约 5 GB 内存。 执行异步工作时,并不总是使用线程。 例如,发出异步 Web 服务请求时,ASP.NET 将不会在 异步 方法调用和 await 之间使用任何线程。 使用线程池为高延迟的请求提供服务可能会导致大量内存占用和服务器硬件利用率不佳。

处理异步请求

在启动时看到大量并发请求或具有突发负载的 Web 应用中, (并发性突然增加) ,使 Web 服务调用异步会提高应用的响应能力。 异步请求与同步请求所需的处理时间相同。 如果请求进行 Web 服务调用需要两秒钟才能完成,则无论请求是同步执行还是异步执行,该请求都需要 2 秒。 但是,在异步调用期间,线程在等待第一个请求完成时不会阻止它响应其他请求。 因此,当存在许多调用长时间运行的操作的并发请求时,异步请求会阻止请求队列和线程池增长。

选择同步操作方法或异步操作方法

本节列出了有关何时使用同步操作方法或异步操作方法的准则。 这些只是准则:单独检查每个应用程序,以确定异步方法是否有助于提高性能。

通常,对于以下情况,请使用同步方法:

  • 操作很简单或运行时间很短。
  • 简单性比效率更重要。
  • 此操作主要是 CPU 操作而不是包含大量的磁盘或网络开销的操作。 对 CPU 绑定操作使用异步操作方法未提供任何好处并且还导致更多的开销。

一般情况下,请对以下情况使用异步方法:

  • 你正在调用可通过异步方法使用的服务,并且使用的是 .NET 4.5 或更高版本。
  • 操作是网络绑定的或 I/O 绑定的而不是 CPU 绑定的。
  • 并行性比代码的简单性更重要。
  • 您希望提供一种可让用户取消长时间运行的请求的机制。
  • 当切换线程的好处超过上下文切换的成本时。 通常,如果同步方法在 ASP.NET 请求线程上等待而不执行任何操作,则应使方法异步。 通过将调用设置为异步,ASP.NET 请求线程在等待 Web 服务请求完成时不会停止不执行任何工作。
  • 测试表明,阻止操作是站点性能的瓶颈,IIS 可以通过对这些阻止调用使用异步方法为更多请求提供服务。

下载的示例演示如何有效地使用异步操作方法。 提供的示例旨在提供使用 .NET 4.5 的 ASP.NET MVC 4 中的异步编程的简单演示。 此示例不用作 ASP.NET MVC 中异步编程的参考体系结构。 示例程序调用 ASP.NET Web API方法,这些方法又调用 Task.Delay 来模拟长时间运行的 Web 服务调用。 大多数生产应用程序不会显示使用异步操作方法的明显优势。

很少有应用程序要求所有的操作方法都是异步的。 通常,将少量的同步操作方法转换为异步方法就会显著增加所需的工作量。

示例应用程序

可以从 GitHub 站点下载示例应用程序https://github.com/RickAndMSFT/Async-ASP.NET/。 存储库由三个项目组成:

  • Mvc4Async:ASP.NET 包含本教程中使用的代码的 MVC 4 项目。 它向 WebAPIpgw 服务发出 Web API 调用。
  • WebAPIpgw:实现 Products, Gizmos and Widgets 控制器 ASP.NET MVC 4 Web API 项目。 它提供 WebAppAsync 项目和 Mvc4Async 项目的数据。
  • WebAppAsync:另一个教程中使用的 ASP.NET Web Forms项目。

Gizmos 同步操作方法

以下代码显示了 Gizmos 用于显示 gizmos 列表的同步操作方法。 (对于本文,小部件是一种虚构的机械设备。)

public ActionResult Gizmos()
{
    ViewBag.SyncOrAsync = "Synchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", gizmoService.GetGizmos());
}

以下代码显示了 GetGizmos gizmo 服务的 方法。

public class GizmoService
{
    public async Task<List<Gizmo>> GetGizmosAsync(
        // Implementation removed.
       
    public List<Gizmo> GetGizmos()
    {
        var uri = Util.getServiceUri("Gizmos");
        using (WebClient webClient = new WebClient())
        {
            return JsonConvert.DeserializeObject<List<Gizmo>>(
                webClient.DownloadString(uri)
            );
        }
    }
}

方法GizmoService GetGizmos将 URI 传递给 ASP.NET Web API HTTP 服务,该服务返回 gizmos 数据的列表。 WebAPIpgw 项目包含 Web API gizmos, widgetproduct控制器的实现。
下图显示了示例项目中的 gizmos 视图。

Gizmos

创建异步 Gizmos 操作方法

此示例使用 .NET 4.5 和 Visual Studio 2012) 中提供的新 asyncawait 关键字 (,让编译器负责维护异步编程所需的复杂转换。 编译器允许使用 C# 的同步控制流构造编写代码,编译器会自动应用使用回调所需的转换,以避免阻塞线程。

以下代码显示了 Gizmos 同步方法和 GizmosAsync 异步方法。 如果浏览器支持 HTML 5 <mark> 元素,则会在黄色突出显示中看到中的 GizmosAsync 更改。

public ActionResult Gizmos()
{
    ViewBag.SyncOrAsync = "Synchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", gizmoService.GetGizmos());
}
public async Task<ActionResult> GizmosAsync()
{
    ViewBag.SyncOrAsync = "Asynchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", await gizmoService.GetGizmosAsync());
}

应用了以下更改以允许 GizmosAsync 异步。

  • 方法标有异步关键字 (keyword) ,告知编译器为正文的某些部分生成回调,并自动创建Task<ActionResult>返回的 。
  • 方法名称中追加了“Async”。 追加“Async”不是必需的,但这是编写异步方法时的约定。
  • 返回类型已从 ActionResult 更改为 Task<ActionResult>。 的 Task<ActionResult> 返回类型表示正在进行的工作,并为方法的调用方提供句柄,通过该句柄等待异步操作完成。 在这种情况下,调用方是 Web 服务。 Task<ActionResult> 表示正在进行的工作,其结果为 ActionResult.
  • await 关键字 (keyword) 已应用于 Web 服务调用。
  • 异步 Web 服务 API (GetGizmosAsync) 调用。

在方法主体内 GetGizmosAsync 调用 GetGizmosAsync 另一个异步方法。 GetGizmosAsync 当数据可用时,立即返回 Task<List<Gizmo>> 最终完成的 。 由于在获得 gizmo 数据之前不想执行任何其他操作,因此代码会使用 await 关键字 (keyword) ) 等待任务 (。 只能在使用异步关键字 (keyword) 批注的方法中使用 await 关键字 (keyword) 。

await 关键字 (keyword) 在任务完成之前不会阻止线程。 它将方法的其余部分注册为任务的回调,并立即返回 。 当等待的任务最终完成时,它将调用该回调,从而在方法停止的位置继续执行。 有关使用 awaitasync 关键字以及 Task 命名空间的详细信息,请参阅 异步引用

以下代码显示了 GetGizmosGetGizmosAsync 方法。

public List<Gizmo> GetGizmos()
{
    var uri = Util.getServiceUri("Gizmos");
    using (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            webClient.DownloadString(uri)
        );
    }
}
public async Task<List<Gizmo>> GetGizmosAsync()
{
    var uri = Util.getServiceUri("Gizmos");
    using (HttpClient httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(uri);
        return (await response.Content.ReadAsAsync<List<Gizmo>>());
    }
}

异步更改类似于上述 GizmosAsync 所做的更改。

  • 方法签名已使用异步关键字 (keyword) 进行批注,返回类型已更改为 Task<List<Gizmo>>Async 已追加到方法名称。
  • 使用异步 HttpClient 类而不是 WebClient 类。
  • await 关键字 (keyword) 已应用于 HttpClient 异步方法。

下图显示了异步 gizmo 视图。

异步

gizmos 数据的浏览器表示形式与同步调用创建的视图相同。 唯一的区别是,异步版本在重负载下的性能可能更高。

并行执行多个操作

当一个操作必须执行多个独立操作时,异步操作方法比同步方法具有显著优势。 在提供的示例中,产品、小组件和 Gizmos) (同步方法 PWG 显示三个 Web 服务调用的结果,以获取产品、小组件和 gizmos 的列表。 提供这些服务的 ASP.NET Web API项目使用 Task.Delay 来模拟延迟或慢速网络调用。 当延迟设置为 500 毫秒时,异步 PWGasync 方法需要略多于 500 毫秒才能完成,而同步 PWG 版本需要超过 1,500 毫秒。 同步 PWG 方法显示在以下代码中。

public ActionResult PWG()
{
    ViewBag.SyncType = "Synchronous";
    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var pwgVM = new ProdGizWidgetVM(
        widgetService.GetWidgets(),
        prodService.GetProducts(),
        gizmoService.GetGizmos()
       );

    return View("PWG", pwgVM);
}

异步 PWGasync 方法显示在以下代码中。

public async Task<ActionResult> PWGasync()
{
    ViewBag.SyncType = "Asynchronous";
    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var widgetTask = widgetService.GetWidgetsAsync();
    var prodTask = prodService.GetProductsAsync();
    var gizmoTask = gizmoService.GetGizmosAsync();

    await Task.WhenAll(widgetTask, prodTask, gizmoTask);

    var pwgVM = new ProdGizWidgetVM(
       widgetTask.Result,
       prodTask.Result,
       gizmoTask.Result
       );

    return View("PWG", pwgVM);
}

下图显示了从 PWGasync 方法返回的视图。

pwgAsync

使用取消令牌

异步操作方法的返回Task<ActionResult>是可取消的,也就是说,当向一个参数提供 AsyncTimeout 属性时,它们会采用 CancellationToken 参数。 以下代码演示 GizmosCancelAsync 超时为 150 毫秒的方法。

[AsyncTimeout(150)]
[HandleError(ExceptionType = typeof(TimeoutException),
                                    View = "TimeoutError")]
public async Task<ActionResult> GizmosCancelAsync(
                       CancellationToken cancellationToken )
{
    ViewBag.SyncOrAsync = "Asynchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos",
        await gizmoService.GetGizmosAsync(cancellationToken));
}

以下代码演示 GetGizmosAsync 重载,该重载采用 CancellationToken 参数。

public async Task<List<Gizmo>> GetGizmosAsync(string uri,
    CancellationToken cancelToken = default(CancellationToken))
{
    using (HttpClient httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(uri, cancelToken);
        return (await response.Content.ReadAsAsync<List<Gizmo>>());
    }
}

在提供的示例应用程序中,选择 “取消令牌演示 ”链接会调用 GizmosCancelAsync 方法并演示异步调用的取消。

高并发/高延迟 Web 服务调用的服务器配置

若要实现异步 Web 应用程序的优势,可能需要对默认服务器配置进行一些更改。 配置和压力测试异步 Web 应用程序时,请记住以下事项。