使用 ASP.NET 4.5 中的非同步方法

作者: Rick Anderson

本教學課程將教導您使用Visual Studio Express 2012 for Web建置非同步 ASP.NET Web Forms應用程式的基本概念,這是 Microsoft Visual Studio 的免費版本。 您也可以使用 Visual Studio 2012。 本教學課程包含下列各節。

本教學課程提供完整的範例,網址為
https://github.com/RickAndMSFT/Async-ASP.NET/GitHub 網站上的 。

ASP.NET 結合 .NET 4.5 的 4.5 網頁可讓您註冊傳回 Task類型物件的非同步方法。 .NET Framework 4 引進了稱為Task的非同步程式設計概念,ASP.NET 4.5 支援Task。 工作是以System.Threading.Tasks命名空間中的Task類型和相關類型來表示。 .NET Framework 4.5 是以awaitasync關鍵字為基礎的非同步支援為基礎,讓使用Task物件比先前的非同步方法更不復雜。 await關鍵字是語法的速記,表示程式碼片段應該以非同步方式等候一些其他程式碼片段。 async關鍵字代表可用來將方法標示為以工作為基礎的非同步方法的提示。 awaitasyncTask物件的組合可讓您更輕鬆地在 .NET 4.5 中撰寫非同步程式碼。 非同步方法的新模型稱為 TASK 架構非同步模式 , (TAP) 。 本教學課程假設您已熟悉使用 awaitasync 關鍵字和 Task 命名空間的非同步程式設計。

如需使用 awaitasync 關鍵字和 Task 命名空間的詳細資訊,請參閱下列參考。

執行緒集區處理要求的方式

在 Web 服務器上,.NET Framework會維護用來服務 ASP.NET 要求的執行緒集區。 要求到達網頁伺服器時,集區中會分派一個執行緒來處理該要求。 如果要求是以同步方式處理,處理要求的執行緒會在處理要求時忙碌中,而且該執行緒無法服務另一個要求。

這可能不是問題,因為執行緒集區的大小可能足以容納許多忙碌執行緒。 不過,執行緒集區中的執行緒數目會受到限制, (.NET 4.5 的預設最大值為 5,000) 。 在具有高並行長時間執行要求的大型應用程式中,所有可用的執行緒可能會忙碌中。 這種狀況稱為「執行緒耗盡」(Thread Starvation)。 達到此條件時,Web 服務器會將要求排入佇列。 如果要求佇列已滿,網頁伺服器會拒絕 HTTP 503 狀態為 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 MVC 4 Web API 專案 Products, Gizmos and Widgets 。 它會提供 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 傳遞至傳回 gizmos 資料的 ASP.NET Web API HTTP 服務。 WebAPIpgw專案包含 Web API gizmos, widgetproduct 控制器的實作。
下圖顯示範例專案中的 gizmos 頁面。

[同步 Gizmos 網頁瀏覽器] 頁面的螢幕擷取畫面,其中顯示 gizmos 的資料表,其中包含輸入至 Web API 控制器的對應詳細資料。

建立異步 Gizmos 頁面

此範例會使用 .NET 4.5 和 Visual Studio 2012) 中提供的新 非同步await 關鍵字 (,讓編譯器負責維護非同步程式設計所需的複雜轉換。 編譯器可讓您使用 C# 的同步控制流程建構來撰寫程式碼,而編譯器會自動套用使用回呼所需的轉換,以避免封鎖執行緒。

ASP.NET 非同步頁面必須包含 Page 指示詞, Async 並將 屬性設定為 「true」。 下列程式碼顯示 Page 指示詞, Async 其中屬性設定為 GizmosAsync.aspx 頁面的 「true」。

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

下列程式碼顯示 Gizmos 同步 Page_Load 方法和 GizmosAsync 非同步頁面。 如果您的瀏覽器支援 HTML 5 < 標記 > 元素,您會看到黃色醒目提示中的 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 方法會以 async 關鍵字標示,告知編譯器產生本文部分的回呼,並自動建立 Task 傳回的 。
  • 「Async」 已附加至非同步方法名稱。 不需要附加 「Async」,而是撰寫非同步方法時的慣例。
  • GetGizmosSvcAsync 方法的傳回型別為 Task 。 的 Task 傳回型別代表進行中的工作,並提供方法的呼叫端控制碼,以便等候非同步作業完成。
  • await關鍵字已套用至 Web 服務呼叫。
  • 非同步 Web 服務 API 稱為 (GetGizmosAsync) 。

GetGizmosSvcAsync 方法主體內呼叫另一個非同步方法 GetGizmosAsyncGetGizmosAsync 會立即傳回 , Task<List<Gizmo>> 當資料可供使用時,最終會完成。 因為您不想在有 gizmo 資料之前執行任何其他動作,所以程式碼會使用 await 關鍵字) 等候工作 (。 您只能在以async關鍵字標注的方法中使用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 (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            await webClient.DownloadStringTaskAsync(uri)
        );
    }
}

非同步變更類似于上述 GizmosAsync 所做的變更。

  • 方法簽章已使用 async 關鍵字加上批註,傳回型別已變更為 Task<List<Gizmo>> ,而 Async 已附加至方法名稱。
  • 系統會使用非同步 HttpClient 類別,而不是同步 WebClient 類別。
  • await關鍵字已套用至HttpClientGetAsync非同步方法。

下圖顯示非同步 gizmo 檢視。

Gizmos Async 網頁瀏覽器頁面的螢幕擷取畫面,其中顯示 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 套用) 等事件處理常式的相同不確定順序。

同時執行多個作業

當動作必須執行數個獨立作業時,非同步方法在同步方法上具有顯著優勢。 在提供的範例中,產品、Widget 和 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 頁面傳回的檢視。

顯示 Widget、Products 和 Gizmos 資料表之非同步 Widget、Products 和 Gizmos 網頁瀏覽器頁面的螢幕擷取畫面。

使用取消權杖

傳回 Task 的非同步方法是可取消的,也就是說,當有一個與 AsyncTimeoutPage指示詞的 屬性一起提供時,它們會採用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 應用程式時,請記住下列事項。

  • Windows 7、Windows Vista、Window 8 和所有 Windows 用戶端作業系統最多有 10 個並行要求。 您需要 Windows Server 作業系統,才能在高負載下查看非同步方法的優點。

  • 使用下列命令,從提升許可權的命令提示字元向 IIS 註冊 .NET 4.5:
    %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 管理員並流覽至 [應用程式集區] 窗格。
    • 以滑鼠右鍵按一下目標應用程式集區,然後選取 [ 進階設定]。
      Internet Information Services Manager 的螢幕擷取畫面,其中顯示醒目提示紅色矩形的 [進階設定] 功能表。
    • 在 [ 進階設定 ] 對話方塊中,將 [佇列長度 ] 從 1,000 變更為 5,000。
      [進階設定] 對話方塊的螢幕擷取畫面,其中顯示 [佇列長度] 欄位設定為 1000,並以紅色矩形反白顯示。

    請注意,在上述映射中,即使應用程式集區使用 .NET 4.5,.NET Framework 仍會列為 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 應用程式,此功能受限於 autoConfig 功能為 CPU 數目的 12 倍。 這表示在四進程上,您最多可以有 12 * 4 = 48 個並行連線到 IP 端點。 由於這會系結至autoConfig,因此在 ASP.NET 應用程式中增加 maxconnection 最簡單的方式,就是在 Application_Startglobal.asax檔案的 from 方法中,以程式設計方式設定System.Net.ServicePointManager.DefaultConnectionLimit。 如需範例,請參閱範例下載。

  • 在 .NET 4.5 中, MaxConcurrentRequestsPerCPU 的預設值為 5000 應該沒問題。

參與者