在ASP.NET MVC中使用非同步作業的時候,我有這麼幾個關注點。非同步作業何時提高我應用的效能,什麼時候沒改善?
在ASP.NET MVC中到處使用非同步作業真的好嗎?
對於可等待的(awaitable)方法: 當查詢資料庫時(通過EF/BHibernate/其他的ORM)應該使用async/await關鍵字嗎?
在一個單獨的操作方法中,非同步地查詢資料庫可以使用await關鍵字多少次?
當一個action必須執行多個獨立的長期啟動並執行操作時,非同步action方法是很有用的。
假設我有三個操作,分別耗時500, 600和700毫秒。採用同步調用的話,總共的回應時間將會稍微超過1800毫秒。然而,如果是非同步呼叫(並發),總共回應時間將會稍微超過700毫秒,因為那是最長的任務/操作的期間。
非同步控制器類一個經典的用法是用於長期啟動並執行Web服務調用。
資料庫應該非同步呼叫嗎?
IIS線程池經常可能處理比一個資料庫伺服器更多的阻塞請求。如果資料庫遇到了瓶頸,那麼非同步調用並不會加速資料庫的響應。由於沒有限流機制,使用非同步呼叫有效地分發更多的任務給一個不知所措的資料庫伺服器只會給資料庫轉移更多的負擔。如果你的資料庫遇到了瓶頸,非同步呼叫不會是魔彈。
而且,非同步並不意味著並發。非同步執行釋放了一個沒有複雜度或效能損耗的外部資源阻塞的有價值的線程池線程。這意味著相同的IIS機器可以處理更多的並發請求,而不是它將啟動並執行更快。
可以在MSDN上看一下這篇文章。作者花了很多精力在這篇部落格上描述何時應該在ASP.NET中使用async而不是如何使用。
答標題問題:
首先,要理解async/await是釋放線程的根本。在GUI應用中,主要釋放GUI線程,因此使用者體驗更好。在伺服器應用(包括ASP.NET MVC)中,主要釋放請求線程,因此伺服器可以擴充。
特別地,它不會:
讓你的個人請求完成的更快。事實上,他們會完成的(只有一點點)更慢。當遇到一個await時,就會返回到調用者/瀏覽器。await只會向ASP.NET線程池“屈服”,而不是瀏覽器。
答問題1:
當你要進行I/O時,我可以說到處使用非同步作業是好的。雖然它可能不一定是有益的(看下面)。
然而,對於CPU受限的方法使用非同步作業是不好的。有時開發人員認為通過在控制器中調用Task.Run可以獲得async的好處,這是一個可怕的觀點。因為那樣的代碼通過開啟其他線程來結束釋放該請求線程,因此根本沒有受益(事實上,他們消耗了額外線程切換的開銷)。
答問題2:
你可以使用你可利用的任何可等待的方法。如果你的ORM不支援async,那麼不要嘗試把它封裝在Task.Run或者任何和那個相似的東西裡。
注意我說的是“你可以使用”。如果你在討論一個單資料庫的ASP.NET MVC,那麼(基本上確定)你不會從async獲得任何伸縮性的好處。這是因為IIS可以比一個SQL Server(或者其他經典的RDBMS)的單一執行個體處理更多的並發請求。然而,如果你的後端是更現代的——SQL server叢集, Azure SQL, NoSQL等等——你的後端可伸縮,並且伸縮性的瓶頸是IIS,那麼你可以從async獲得伸縮性的好處。
答問題3:
你愛用多少次就用多少次,只要你喜歡。然而,注意許多ORM都有“每次串連一次操作”的原則。特別的,EF每個DbContext只允許一個單獨的操作;無論該操作是同步的還是非同步,這都成立。
而且,再次記住你後端的伸縮性。如果你碰上了一個單一執行個體的SQL Server,且IIS已經可以使SQL Server處於滿負荷狀態,那麼SQL Server雙倍或三倍的壓力對你一點好處都沒有。
在 ASP.NET MVC 中使用非同步控制器
線程池處理請求的方式
在 Web 服務器上,.NET Framework 維護一個用於服務 ASP.NET 請求的線程池。 當請求到達時,將調度池中的線程以處理該請求。 如果對請求進行同步處理,則在處理請求時將阻塞處理請求的線程,並且該線程不能對另一個請求提供服務。
這可能不是一個問題,因為線程池可以設定得足夠大以容納許多阻塞的線程。 但是,線程池中的線程數目是有限制的。 在同時處理多個長時間啟動並執行請求的大型應用程式中,可能會阻塞所有可用的線程。 這種情況稱為“線程不足”。 當出現這種情況時,Web 服務器會將請求排隊。 如果請求隊列已滿,則 Web 服務器會拒絕請求並處於 HTTP 503 狀態(伺服器太忙)。
處理非同步請求
在可能出現線程不足的應用程式中,您可以配置通過非同步方式處理操作。 非同步請求與同步請求所需的處理時間相同。 例如,如果某個請求產生一個需要兩秒鐘來完成的網路調用,則該請求無論是同步執行還是非同步執行都需要兩秒鐘。 但是,在非同步呼叫的過程中,伺服器在等待第一個請求完成的過程中不會阻塞對其他請求的響應。 因此,當有許多請求調用長時間啟動並執行操作時,非同步請求可以防止出現請求排隊的情況。
在調用非同步作業時,將執行以下步驟:
Web 服務器從線程池(輔助線程)擷取一個線程並安排它處理傳入請求。 此輔助線程啟動一個非同步作業。
將此輔助線程返回到線程池以對另一個 Web 請求提供服務。
在非同步作業完成時通知 ASP.NET。
Web 服務器從線程池擷取一個線程(可能是與啟動非同步作業的線程不同的線程)以處理請求的其餘部分,包括呈現響應。
下圖顯示了非同步模式。
選擇同步操作方法或非同步作業方法
本節列出了有關何時使用同步操作方法或非同步作業方法的準則。 這隻是一些準則;您必須逐個檢查每個應用程式以確定非同步作業方法是否能協助提高效能。
通常,在滿足以下條件時使用同步管線:
操作很簡單或已耗用時間很短。
簡單性比效率更重要。
此操作主要是 CPU 操作而不是包含大量的磁碟或網路開銷的操作。 對 CPU 綁定操作使用非同步作業方法未提供任何好處並且還導致更多的開銷。
通常,在滿足以下條件時使用非同步管線:
操作是網路綁定的或 I/O 綁定的而不是 CPU 綁定的。
測試顯示阻塞操作對於網站效能是一個瓶頸,並且通過對這些阻塞調用使用非同步作業方法,IIS 可對更多的請求提供服務。
並行性比代碼的簡單性更重要。
您希望提供一種可讓使用者取消長時間啟動並執行請求的機制。
下載的樣本示範如何有效地使用非同步作業方法。 樣本程式調用 Sleep 方法來類比長時間啟動並執行進程。 很少有產品應用程式會顯示出如此明顯的使用非同步作業方法的好處。
您應測試應用程式以確定非同步方法呼叫是否能提供效能好處。 在某些情況下,增加每個 CPU 的 IIS 最大並發請求數和每個 CPU 的最大並發線程數可能會更好。 有關 ASP.NET 線程配置的更多資訊,請參見 Thomas Marquardt 的部落格上的文章 ASP.NET Thread Usage on IIS 7.0 and 6.0(ASP.NET 線程在 IIS 7.0 和 6.0 上的使用方式)。 有關何時執行非同步資料庫調用的更多資訊,請參見 Rick Anderson 部落格上的文章 Should my database calls be Asynchronous?(我的資料庫調用是否應採用非同步方式?)。
很少有應用程式要求所有的操作方法都是非同步。 通常,將少量的同步操作方法轉換為非同步方法呼叫就會顯著增加所需的工作量。
將同步操作方法轉換為非同步作業方法
下面的程式碼範例示範了一個同步操作方法,它用於顯示來自門戶網站控制器的新聞項。 請求 Portal/News?city=Seattle 顯示 Seattle 的新聞。
C#
public class PortalController: Controller {
public ActionResult News(string city) {
NewsService newsService = new NewsService();
ViewStringModel headlines = newsService.GetHeadlines(city);
return View(headlines);
}
}
下面的樣本示範了重新編寫為非同步方法呼叫的 News 操作方法。
C#
public class PortalController : AsyncController {
public void NewsAsync(string city) {
AsyncManager.OutstandingOperations.Increment();
NewsService newsService = new NewsService();
newsService.GetHeadlinesCompleted += (sender, e) =>
{
AsyncManager.Parameters["headlines"] = e.Value;
AsyncManager.OutstandingOperations.Decrement();
};
newsService.GetHeadlinesAsync(city);
}
public ActionResult NewsCompleted(string[] headlines) {
return View("News", new ViewStringModel
{
NewsHeadlines = headlines
});
}
}
將同步操作方法轉換為非同步作業方法包含以下步驟:
不要從 Controller 派生控制器,而應從 AsyncController 派生。 從 AsyncController 派生的控制器使 ASP.NET 能夠處理非同步請求,並且這些控制器仍然可以為同步操作方法提供服務。
為操作建立兩個方法。 啟動非同步進程的方法必須具有一個由操作和尾碼“Async”組成的名稱。 非同步進程完成(回調方法)時調用的方法必須具有一個由操作和尾碼“Completed”組成的名稱。 在前面的樣本中,News 方法已轉換為兩個方法:NewsAsync 和 NewsCompleted。
NewsAsync 方法返回 void(在 Visual Basic 中沒有任何值)。 NewsCompleted 方法返回 ActionResult 執行個體。 儘管操作由兩個方法組成,但使用與同步操作方法相同的 URL 來訪問它(例如 Portal/News?city=Seattle)。 其他方法(例如 RedirectToAction 和 RenderAction)還是將按照 News 而不是 NewsAsync 來引用操作方法。
傳遞到 NewsAsync 的參數使用普通的參數綁定機制。 傳遞到 NewsCompleted 的參數使用 Parameters 字典。
使用非同步作業方法中的非同步呼叫替換原始 ActionResult 方法中的同步調用。 在上面的樣本中,使用對 newsService.GetHeadlinesAsync 的調用替換對 newsService.GetHeadlines 的調用。
由 NewsAsync 方法使用的 NewsService 類是一個使用事件架構非同步模式公開方法的服務樣本。 有關此模式的更多資訊,請參見事件架構非同步模式概述。
OutstandingOperations 屬性通知 ASP.NET 有多少個操作已掛起。 這是必要的,因為 ASP.NET 不能確定由操作方法啟動了多少個操作或這些操作何時完成。 當 OutstandingOperations 屬性為零時,ASP.NET 可通過調用 NewsCompleted 方法來完成整個非同步作業。
請注意下面有關非同步作業方法的一些事項:
如果操作名稱為 Sample,則架構將尋找 SampleAsync 和 SampleCompleted 方法。
視圖頁應命名為 Sample.aspx,而不是命名為 SampleAsync.aspx 或 SampleCompleted.aspx。 (操作名稱為 Sample,而不是為 SampleAsync。)
控制器不能包含名為 SampleAsync 的非同步方法呼叫和名為 Sample 的同步方法。 如果包含這兩個方法,則會引發 AmbiguousMatchException 異常,因為 SampleAsync 操作方法和 Sample 操作方法具有相同的請求籤名。
並存執行多個操作
當操作必須執行幾個獨立的操作時,非同步作業方法很有用。 例如,門戶網站可能不只顯示新聞,還顯示體育、天氣、股票和其他資訊。
下面的樣本示範了新聞門戶網站 Index 操作方法的同步版本。
C#
public ActionResult IndexSynchronous( string city ) {
NewsService newsService = new NewsService();
string[] headlines = newsService.GetHeadlines();
SportsService sportsService = new SportsService();
string[] scores = sportsService.GetScores();
WeatherService weatherService = new WeatherService();
string[] forecast = weatherService.GetForecast();
return View("Common", new PortalViewModel {
NewsHeadlines = headlines,
SportsScores = scores,
Weather = forecast
});
}
按順序執行對每個服務的調用。 因此,為了響應請求所需的時間是每個服務調用的時間加上少量系統開銷的時間的總和。 例如,如果各個調用分別用了 400、500 和 600 毫秒,則總的回應時間將稍微大於 1.5 秒。 但是,如果非同步執行服務調用(以並行方式),則總的回應時間將稍微大於 600 毫秒,因為這是最長任務的期間。
下面的樣本示範了新聞門戶網站 Index 操作方法的非同步版本。
C#
public void IndexAsync(string city) {
AsyncManager.OutstandingOperations.Increment(3);
NewsService newsService = new NewsService();
newsService.GetHeadlinesCompleted += (sender, e) =>
{
AsyncManager.Parameters["headlines"] = e.Value;
AsyncManager.OutstandingOperations.Decrement();
};
newsService.GetHeadlinesAsync();
SportsService sportsService = new SportsService();
sportsService.GetScoresCompleted += (sender, e) =>
{
AsyncManager.Parameters["scores"] = e.Value;
AsyncManager.OutstandingOperations.Decrement();
};
sportsService.GetScoresAsync();
WeatherService weatherService = new WeatherService();
weatherService.GetForecastCompleted += (sender, e) =>
{
AsyncManager.Parameters["forecast"] = e.Value;
AsyncManager.OutstandingOperations.Decrement();
};
weatherService.GetForecastAsync();
}
public ActionResult IndexCompleted(string[] headlines, string[] scores, string[] forecast) {
return View("Common", new PortalViewModel {
NewsHeadlines = headlines,
SportsScores = scores,
Weather = forecast
});
}
}
在前面的樣本中,使用參數 3 調用 Increment 方法,這是因為有三個非同步作業。
將特性添加到非同步作業方法
如果要將特性應用於非同步作業方法,則將它們應用於 ActionAsync 方法,而不是應用於 ActionCompleted 方法。 忽略 ActionCompleted 方法上的特性。
已添加兩個新的特性:AsyncTimeoutAttribute 和 NoAsyncTimeoutAttribute。 這些特性可讓您控制非同步逾時時間。
使用 BeginMethod/EndMethod 模式
如果非同步作業方法調用一個使用 BeginMethod/EndMethod 模式公開方法的服務,則回調方法(即作為非同步回調參數傳遞到 Begin 方法的方法)可能會在一個不由 ASP.NET 控制的線程上執行。 在此情況下,HttpContext.Current 將為 null,並且當應用程式訪問 AsyncManager 類的成員(例如 Parameters)時可能會出現競爭條件。 若要確保已訪問 HttpContext.Current 執行個體並避免競爭條件,則可以通過從回調方法中調用 Sync() 來還原 HttpContext.Current。
如果回調同步完成,則回調將在由 ASP.NET 控制的線程上執行並且將對操作進行序列化,因此不會出現並發問題。 從已經由 ASP.NET 控制的線程中調用 Sync() 具有未定義的行為。
將總是在由 ASP.NET 控制的線程上調用 ActionCompleted 方法。 因此,不要從該方法中調用 Sync()。
傳遞到 Begin 方法的回調可能會使用由 ASP.NET 控制的線程來進行調用。 因此,您必須在調用 Sync() 之前檢查此條件。 如果操作已同步完成(即,如果 CompletedSynchronously 為 true),則回調在原始線程上執行,並且您不必調用 Sync()。 如果操作已非同步完成,(即,CompletedSynchronously 為 false),則回調線上程池或 I/O 完成連接埠線程上執行,並且您必須 Sync()。
有關 BeginMethod/EndMethod 模式的更多資訊,請參見非同步編程概述和 Rick Anderson 的部落格上的文章 Using the BeginMethod/EndMethod pattern with MVC(對 MVC 使用 BeginMethod/EndMethod 模式)。
類參考
下表列出了非同步作業方法的關鍵類。
類 說明
AsyncController 為非同步控制器提供基類。
AsyncManager 為 AsyncController 類提供非同步作業。