在 ASP.NET 4.5 中使用异步方法

作者 :Rick Anderson

本教程将介绍使用 Visual Studio Express 2012 for Web(Microsoft Visual Studio 的免费版本)生成异步 ASP.NET Web Forms应用程序的基础知识。 也可以使用 Visual Studio 2012。 本教程包含以下部分。

本教程提供了完整示例,请参阅
https://github.com/RickAndMSFT/Async-ASP.NET/在 GitHub 站点上。

ASP.NET 4.5 网页与 .NET 4.5 结合使用,可注册返回 Task 类型的对象的异步方法。 .NET Framework 4 引入了称为 Task 的异步编程概念,ASP.NET 4.5 支持 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 个线程)会消耗大约 5 GB 的内存,而应用程序能够使用异步方法为相同的请求提供服务,并且仅占用 50 个线程。 执行异步工作时,并不总是使用线程。 例如,发出异步 Web 服务请求时,ASP.NET 将不会在 异步 方法调用和 await 之间使用任何线程。 使用线程池为高延迟的请求提供服务可能会导致内存占用大,服务器硬件利用率低。

处理异步请求

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

选择同步或异步方法

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

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

  • 操作很简单或运行时间很短。
  • 简单性比效率更重要。
  • 此操作主要是 CPU 操作而不是包含大量的磁盘或网络开销的操作。 对 CPU 绑定操作使用异步方法没有好处,而且会产生更多开销。

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

  • 你正在调用可通过异步方法使用的服务,并且使用的是 .NET 4.5 或更高版本。

  • 操作是网络绑定的或 I/O 绑定的而不是 CPU 绑定的。

  • 并行性比代码的简单性更重要。

  • 您希望提供一种可让用户取消长时间运行的请求的机制。

  • 当切换线程的好处超过上下文切换的成本时。 通常,如果同步方法阻止 ASP.NET 请求线程而不执行任何工作,则应使方法异步。 通过将调用设置为异步,ASP.NET 请求线程在等待 Web 服务请求完成时不会阻止它不执行任何工作。

  • 测试表明,阻止操作是站点性能的瓶颈,IIS 可以通过为这些阻止调用使用异步方法为更多请求提供服务。

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

很少有应用程序要求所有方法都是异步的。 通常,将一些同步方法转换为异步方法可以最大程度地提高所需的工作量的效率。

示例应用程序

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

  • WebAppAsync:使用 Web API WebAPIpwg 服务的 ASP.NET Web Forms项目。 本教程的大部分代码来自此项目。
  • WebAPIpgw:ASP.NET 实现控制器的 Products, Gizmos and Widgets MVC 4 Web API 项目。 它提供 WebAppAsync 项目和 Mvc4Async 项目的数据。
  • Mvc4Async:ASP.NET 包含另一教程中使用的代码的 MVC 4 项目。 它会对 WebAPIpwg 服务进行 Web API 调用。

Gizmos 同步页

以下代码演示 Page_Load 用于显示 gizmos 列表的同步方法。 (对于本文,gizmo 是一种虚构的机械设备。)

public partial class Gizmos : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        var gizmoService = new GizmoService();
        GizmoGridView.DataSource = gizmoService.GetGizmos();
        GizmoGridView.DataBind();
    }
}

以下代码显示了 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 Web 浏览器”页面的屏幕截图,其中显示了 gizmos 的表,其中包含输入到 Web API 控制器中的相应详细信息。

创建异步 Gizmos 页

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

ASP.NET 异步页面必须包含 属性 设置为“true”的 Async Page 指令。 以下代码显示了 GizmosAsync.aspx 页的 属性设置为“true”的 Page 指令Async

<%@ Page Async="true"  Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosAsync.aspx.cs" Inherits="WebAppAsync.GizmosAsync" %>

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

protected void Page_Load(object sender, EventArgs e)
{
   var gizmoService = new GizmoService();
   GizmoGridView.DataSource = gizmoService.GetGizmos();
   GizmoGridView.DataBind();
}

异步版本:

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcAsync));
}

private async Task GetGizmosSvcAsync()
{
    var gizmoService = new GizmoService();
    GizmosGridView.DataSource = await gizmoService.GetGizmosAsync();
    GizmosGridView.DataBind();
}

应用了以下更改以允许 GizmosAsync 页面是异步的。

  • Page 指令必须将 Async 属性设置为“true”。
  • 方法 RegisterAsyncTask 用于注册包含异步运行的代码的异步任务。
  • GetGizmosSvcAsync方法标有异步关键字 (keyword) ,告知编译器为正文的各个部分生成回调,并自动创建Task返回的 。
  • “Async”已追加到异步方法名称。 追加“Async”不是必需的,但这是编写异步方法时的约定。
  • GetGizmosSvcAsync 方法的返回类型为 Task。 的 Task 返回类型表示正在进行的工作,并为方法的调用方提供句柄,通过该句柄等待异步操作完成。
  • await 关键字 (keyword) 已应用于 Web 服务调用。
  • 异步 Web 服务 API (GetGizmosAsync) 调用。

在方法主体内 GetGizmosSvcAsync 调用 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 (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            await webClient.DownloadStringTaskAsync(uri)
        );
    }
}

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

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

下图显示了异步 gizmo 视图。

Gizmos Async Web 浏览器页的屏幕截图,其中显示了 gizmos 表,其中包含输入到 Web API 控制器中的相应详细信息。

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

RegisterAsyncTask 说明

RegisterAsyncTask 挂钩的方法将在 PreRender 后立即运行。

如果直接使用 async void 页面事件,如以下代码所示:

protected async void Page_Load(object sender, EventArgs e) {
    await ...;
    // do work
}

你不再完全控制事件何时执行。 例如,如果 .aspx 和 。主定义 Page_Load 事件,其中一个或两个事件都是异步的,无法保证执行顺序。 适用于事件处理程序 (相同的不确定顺序,例如 async void Button_Click ) 。

并行执行多个操作

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

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();

    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var pwgVM = new ProdGizWidgetVM(
        widgetService.GetWidgets(),
        prodService.GetProducts(),
        gizmoService.GetGizmos()
       );
    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();

    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}", 
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

异步 PWGasync 代码隐藏如下所示。

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();
    RegisterAsyncTask(new PageAsyncTask(GetPWGsrvAsync));
    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}",
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

private async Task GetPWGsrvAsync()
{
    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
       );

    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();           
}

下图显示了从异步 PWGasync.aspx 页返回的视图。

“异步小组件、产品和 Gizmos”Web 浏览器页的屏幕截图,其中显示了小组件、产品和 Gizmos 表。

使用取消令牌

异步方法返回Task是可取消的,即当向 Page 指令的 属性提供AsyncTimeout一个 CancellationToken 参数时,它们采用一个 CancellationToken 参数。 以下代码显示 GizmosCancelAsync.aspx 页,其超时时间为秒。

<%@ Page  Async="true"  AsyncTimeout="1" 
    Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosCancelAsync.aspx.cs" 
    Inherits="WebAppAsync.GizmosCancelAsync" %>

以下代码显示了 GizmosCancelAsync.aspx.cs 文件。

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcCancelAsync));
}

private async Task GetGizmosSvcCancelAsync(CancellationToken cancellationToken)
{
    var gizmoService = new GizmoService();
    var gizmoList = await gizmoService.GetGizmosAsync(cancellationToken);
    GizmosGridView.DataSource = gizmoList;
    GizmosGridView.DataBind();
}
private void Page_Error(object sender, EventArgs e)
{
    Exception exc = Server.GetLastError();

    if (exc is TimeoutException)
    {
        // Pass the error on to the Timeout Error page
        Server.Transfer("TimeoutErrorPage.aspx", true);
    }
}

在提供的示例应用程序中,选择 GizmosCancelAsync 链接会调用 GizmosCancelAsync.aspx 页,并通过超时异步调用) 来演示取消 (。 由于延迟时间在随机范围内,因此可能需要刷新页面几次才能获取超时错误消息。

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

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

作者