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

作者 :Rick Anderson

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

Github 上提供本教學課程的完整範例 https://github.com/RickAndMSFT/Async-ASP.NET/

組合.NET 4.5的 ASP.NET MVC 4控制器類別可讓您撰寫非同步動作方法,以傳回Task < ActionResult >類型的物件。 .NET Framework 4 引進了稱為Task的非同步程式設計概念,ASP.NET MVC 4 支援Task。 工作是由System.Threading.Tasks命名空間中的Task類型和相關類型來表示。 .NET Framework 4.5 是以awaitasync關鍵字為基礎的這個非同步支援為基礎,讓使用Task物件比先前的非同步方法更複雜。 await關鍵字是語法簡短的,表示程式碼片段應該以非同步方式等候一些其他程式碼片段。 async關鍵字代表可用來將方法標示為工作型非同步方法的提示。 awaitasyncTask物件的組合可讓您更輕鬆地在 .NET 4.5 中撰寫非同步程式碼。 非同步方法的新模型稱為工作 型非同步模式 (TAP) 。 本教學課程假設您已熟悉使用 awaitasync 關鍵字和 Task 命名空間進行非同步程式設計。

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

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

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

這可能不是問題,因為執行緒集區可以足夠容納許多忙碌執行緒。 不過,執行緒集區中的執行緒數目會受到限制, (.NET 4.5 的預設最大值為 5,000) 。 在具有高並行處理長時間執行要求的大型應用程式中,所有可用的執行緒可能會忙碌中。 這種狀況稱為「執行緒耗盡」(Thread Starvation)。 達到此條件時,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:實作 Products, Gizmos and Widgets 控制器的 ASP.NET MVC 4 Web API 專案。 它會提供 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

建立異步 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 非同步。

  • 方法會以 async 關鍵字標示,告知編譯器產生主體部分的回呼,並自動建立 Task<ActionResult> 傳回的 。
  • 「Async」 已附加至方法名稱。 撰寫非同步方法時,不需要附加 「Async」,但這是慣例。
  • 傳回型別已從 ActionResult 變更為 Task<ActionResult> 。 的傳回型 Task<ActionResult> 別代表進行中的工作,並提供方法的呼叫端,以等候非同步作業完成的控制碼。 在此情況下,呼叫端是 Web 服務。 Task<ActionResult> 表示使用 結果進行中的工作 ActionResult.
  • await關鍵字已套用至 Web 服務呼叫。
  • 非同步 Web 服務 API 稱為 (GetGizmosAsync) 。

GetGizmosAsync在方法主體內呼叫另一個非同步方法 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 (HttpClient httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(uri);
        return (await response.Content.ReadAsAsync<List<Gizmo>>());
    }
}

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

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

下圖顯示非同步 gizmo 檢視。

async

gizmos 資料的瀏覽器呈現方式與同步呼叫所建立的檢視相同。 唯一的差異在於非同步版本在大量負載下可能更具效能。

同時執行多個作業

當動作必須執行數個獨立作業時,非同步動作方法優於同步方法。 在提供的範例中,Products、Widgets 和 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 應用程式時,請記住下列事項。

  • Windows 7、Windows Vista 和所有 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 管理員並流覽至 [應用程式集區] 窗格。
    • 以滑鼠右鍵按一下目標應用程式集區,然後選取 [ 進階設定]。
      先進
    • 在 [ 進階設定 ] 對話方塊中,將 [佇列長度 ] 從 1,000 變更為 5,000。
      佇列長度

    請注意,在上述映射中,即使應用程式集區使用 .NET 4.5,.NET Framework 仍會列為 v4.0。 若要瞭解此差異,請參閱下列內容:

  • 如果您的應用程式使用 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 應該沒問題。