作者 :里克·安德森

本教程介绍如何使用 Visual Studio Express 2012 for Web 构建异步 ASP.NET MVC Web 应用程序,这是 Microsoft Visual Studio 的免费版本。 还可以使用 Visual Studio 2012

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

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

有关 using await async 关键字和 Task 命名空间的详细信息,请参阅以下参考。

  • 白皮书:.NET 中的 Asynchrony
  • Async/Await 常见问题解答
  • Visual Studio 异步编程
  • 线程池处理请求的方式

    在 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 可以使用异步方法对这些阻塞调用提供更多请求。
  • 下载的示例演示如何有效地使用异步操作方法。 提供的示例旨在使用 .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 :实现控制器的 ASP.NET MVC 4 Web API 项目 Products, Gizmos and Widgets 。 它为 WebAppAsync 项目和 Mvc4Async 项目提供数据。
  • WebAppAsync :另一个教程中使用的 ASP.NET Web Forms项目。
  • Gizmos 同步操作方法

    以下代码显示了 Gizmos 用于显示 gizmos 列表的同步操作方法。 (在本文中,gizmo 是虚构的机械设备。)

    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 操作方法

    此示例使用 .NET 4.5 和 Visual Studio 2012) 中提供的新 异步await (关键字,让编译器负责维护异步编程所需的复杂转换。 编译器允许你使用 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 异步操作。

  • 该方法使用 异步 关键字进行标记,该关键字指示编译器为正文的各个部分生成回调,并自动创建 Task<ActionResult> 返回的回调。
  • “Async”已追加到方法名称中。 不需要追加“Async”,但在编写异步方法时是约定。
  • 返回类型已从 ActionResult 更改为 Task<ActionResult>。 返回类型 Task<ActionResult> 表示正在进行的工作,并为方法的调用方提供等待异步操作完成的句柄。 在这种情况下,调用方是 Web 服务。 Task<ActionResult> 表示正在处理的结果 ActionResult.
  • await 关键字已应用于 Web 服务调用。
  • 异步 Web 服务 API () GetGizmosAsync 调用。
  • GetGizmosAsync在方法正文中调用GetGizmosAsync另一个异步方法。 GetGizmosAsync 立即返回一个最终将在数据可用时完成的项 Task<List<Gizmo>> 。 由于在具有 gizmo 数据之前,你不想执行任何其他操作,因此代码会等待任务 (使用 await 关键字) 。 只能在用异步关键字批注的方法中使用 await 关键字。

    await 关键字在任务完成之前不会阻止线程。 它将该方法的其余部分注册为任务的回调,并立即返回。 当等待的任务最终完成时,它将调用该回调,从而继续执行从其离开位置的方法。 有关使用 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 所做的更改。

  • 方法签名用 异步 关键字批注,返回类型已更改为 Task<List<Gizmo>>Async 已追加到方法名称。
  • 使用异步 HttpClient 类而不是 WebClient 类。
  • await 关键字已应用于 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 方法返回的视图。

    使用取消令牌

    返回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 应用程序时,请记住以下几点。

  • Windows 7、Windows Vista 和所有 Windows 客户端操作系统最多有 10 个并发请求。 需要 Windows Server 操作系统才能在高负载下查看异步方法的优点。

  • 从提升的命令提示符将 .NET 4.5 注册到 IIS:
    %windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_regiis -i
    请参阅 ASP.NET IIS 注册工具 (Aspnet_regiis.exe)

  • 可能需要将 HTTP.sys 队列限制从默认值 1,000 增加到 5,000。 如果设置太低,你可能会看到 HTTP.sys 拒绝具有 HTTP 503 状态的请求。 更改HTTP.sys队列限制:

  • 打开 IIS 管理器并导航到“应用程序池”窗格。
  • 右键单击目标应用程序池,然后选择“高级设置”。
  • “高级设置” 对话框中,将 队列长度 从 1,000 更改为 5,000。

    请注意,在上面的映像中,即使应用程序池使用的是 .NET 4.5,.NET 框架也会列为 v4.0。 若要了解此差异,请参阅以下内容:

  • .NET 版本控制与多目标 - .NET 4.5 是到 .NET 4.0 的就地升级
  • 如何将 IIS 应用程序或 AppPool 设置为使用 ASP.NET 3.5 而不是 2.0
  • .NET Framework 版本和依赖关系
  • 如果应用程序使用 Web 服务或 System.NET 通过 HTTP 与后端通信,则可能需要增加 connectionManagement/maxconnection 元素。 对于 ASP.NET 应用程序,此功能受自动配置功能限制为 CPU 数的 12 倍。 这意味着,在四进程上,最多可以有 12 * 4 = 48 个到 IP 终结点的并发连接。 由于这与 autoConfig 相关联,因此在 ASP.NET 应用程序中增加maxconnection的最简单方法是在 global.asax 文件中的方法中Application_Start以编程方式设置 System.Net.ServicePointManager.DefaultConnectionLimit。 有关示例,请参阅示例下载。

  • 在 .NET 4.5 中, MaxConcurrentRequestsPerCPU 的默认值应为 5000。

  •