The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

ASP.NET MVC 開發心得分享 (18):非同步控制器開發

ASP.NET MVC 2.0 新增 非同步控制器 (AsyncController) 的開發方式,若是你的網站流量大但是大部分的運算資源是落在與 CPU 無關的運算上時,例如 SQL Server、讀取外部網路資源、呼叫外部 Web Service 等等,就很適合利用此功能來開發 ASP.NET MVC 應用程式,由於這是個比較進階且冷門的技術,所以我大致寫一些 IIS 處理非同步要求的原理與 ASP.NET MVC 非同步控制器的撰寫方式以及開發應注意的事項。

IIS 如何處理所有從用戶端過來的需求

在 IIS 中會由 .NET Framework 控管一個 執行緒池 (ThreadPool),每當要求到達時就會從 ThreadPool 中分配一個 Thread 來處理這個要求,如果你使用同步的方式處理這個要求就會佔用掉這一個 Thread (大部分網頁的處理方式),並且等程式執行完畢到回應資料到用戶端這端時間都會佔用掉一個執行緒。

但是一台機器的 ThreadPool 可提供的 Threads 數量有限,當 ThreadPool 所使用的 Threads 已經到達上限,IIS 就會先將用戶端來的要求加入要求佇列(Request Queue),當加入要求佇列抵達 IIS 上限時,IIS 就會暫時無法提供服務,並回應 HTTP 503 服務無法使用 的錯誤訊息,IIS 設定佇列長度參考如下圖示:

要解決這個問題就可以透過「非同步程式設計模式」的開發方式,讓程式開始執行之後就先將用來處理要求的 Thread 歸還給 IIS,這樣就不會佔用 IIS 所提供的 ThreadPool 來執行這些工作,而 IIS 就能夠再接收其他用戶端要求了,而實際的執行動作由另一條獨立的 Thread 來執行,等到執行完畢再去跟 IIS 的 ThreadPool 請求另一條 Thread 來回應執行結果到用戶端。

ASP.NET MVC 如何開發非同步控制器

以下是我寫的一個簡易 ASP.NET MVC 非同步控制器範例,他會在取抓取 http://www.asp.net/ 的網頁內容,並將網頁內容回應到用戶端:

using System;
using System.Web.Mvc;
using System.Net;
using System.IO;

namespace MvcApplication1.Controllers
{
    public class HomeController : AsyncController
    {
        [AsyncTimeout(5000)] // 設定Action的逾期時間為5秒鐘
        public void IndexAsync()
        {
            AsyncManager.OutstandingOperations.Increment(1);

            WebRequest req = HttpWebRequest.Create("http://www.asp.net/");
            req.BeginGetResponse((ar) =>
            {
                WebResponse response = req.EndGetResponse(ar);
                Stream resp = response.GetResponseStream();
                using (var sr = new StreamReader(resp))
                {
                    AsyncManager.Parameters["html"] = sr.ReadToEnd();
                    AsyncManager.OutstandingOperations.Decrement();
                }
            }, null);
        }

        public ActionResult IndexCompleted(string html)
        {
            return View((object)html);
        }
    }
}

透過這個範例,我摘要列出幾個開發非同步控制器時需要注意的地方:

  • 控制器類別必須繼承自 AsyncController 類別
  • 如果 Action 名稱為 Index,就會有兩個 public method 需定義,分別是:
    • IndexAsync
      • 可接收 Model Binding 過來的值
      • 回傳型別必須為 void
      • 可套用 Action Filter ( 只能在這個 method 套用,不能套用在 IndexCompleted 上面 )
      • 該 method 內的程式碼也要以非同步程式設計模式來開發程式才有意義!
      • 若要傳資料給 IndexCompleted 使用,可以透過 AsyncManager.Parameters 傳值
    • IndexCompleted
      • 回傳型別可以是 void、null、ActionResult 或其他 object,與原本的 ASP.NET MVC 回傳的型別規則是一樣的。
      • 傳入的參數名稱可以用 AsyncManager.Parameters 設定的 Key 名稱來當參數名稱,這裡會自動接到這些參數的內容。

ASP.NET MVC 非同步控制器執行流程說明

透過 ASP.NET MVC 非同步控制器執行 Action 大致會是以下流程:

  1. 從 IIS 取得一條執行緒,並用來處理執行 IndexAsync 方法,此方法會啟動額外的非同步作業 (透過非同步程式設計模式來開發)
  2. IndexAsync 方法執行完後會將執行緒歸還到 ThreadPool 中
  3. 當額外啟動的 非同步作業 處理完成時,會通知 ASP.NET MVC 已完成執行
    • IndexAsync 開始執行非同步程式前需利用 AsyncManager 控制非同步的執行狀態,如果你只有一件非同步工作要執行,那麼你一開始要先將非同步執行的計數器加 1,當非同步程式執行完成後要減 1,ASP.NET MVC 會監控 AsyncManager.OutstandingOperations 內部的操作計數器(OperationCounter),在操作計數器歸 0 的時候 ASP.NET MVC 框架就會被告知要去執行 IndexCompleted 方法以完成此要求,例如:
      • AsyncManager.OutstandingOperations.Increment(1);
      • AsyncManager.OutstandingOperations.Decrement();
    • 通常 AsyncManager.OutstandingOperations.Decrement(); 這段程式都是在由 IndexAsync 啟動的非同步作業中執行,否則只要操作計數器 歸 0 後就會開始執行 IndexCompleted  並回應用戶端了!
  4. 最後再從 IIS 取得一條執行緒,並用來處理執行 IndexCompleted 方法,主要是用來回應資料到用戶端。

注意事項

  • 你就算繼承 AsyncController 類別還是能使用同步的方式撰寫 Action,就跟原本的開發方式一樣。
  • 如果你套用 AsyncTimeoutNoAsyncTimeout 屬性(Action Filter),其 Timeout 的時間計算是從 IndexAsync 回傳後才開始起算。
  • IndexAsyncIndexCompleted 這兩個取得的 Thread 不一定會相同,因為是分兩次去 ThreadPool 取得 Thread 的。
  • 你不能同時定義 Index (同步的方式) , IndexAsync , IndexCompleted 這三個 public method 否則會引發例外狀況,如下錯誤畫面:

相關連結