本文章是由機器翻譯。

非同步程式設計

ASP.NET 上的 Async/Await 簡介

Stephen Cleary

大多數的線上資源,周圍等待非同步/假設您正在開發用戶端應用程式,但是非同步沒有在伺服器上的一個地方嗎?答案是大多數肯定"是的"。這篇文章是ASP.NET,以及借鑒最好的連線資源的非同步請求的概念性概述。我不會被覆蓋的非同步或等待語法 ; 我已經做到在入門篇博文中 (bit.ly/19IkogW) 和非同步最佳做法的一篇文章中 (msdn.microsoft.com/magazine/jj991977)。這篇文章具體側重于非同步上ASP.NET的工作方式。

對於用戶端應用程式如 Windows 應用商店,Windows 桌面和 Windows Phone 的應用程式,非同步主要優點是回應。這些類型的應用程式使用非同步主要是為了保持 UI 的回應。對於伺服器應用程式,非同步主要優點是可擴充性。Node.js 可伸縮性的關鍵是其天生就是非同步性質 ; 打開 Web 介面為.NET (浩然) 旨在從地面上是非同步 ; 和ASP.NET也可以是非同步。非同步:它不是只為 UI 的應用程式 !

同步 vs。 非同步請求處理

在進非同步請求處理常式之前,我想簡要回顧一下如何同步請求處理常式在ASP.NET工作。 對於此示例,假設系統中的請求取決於一些外部的資源,比如資料庫或 Web API。當一個請求時,ASP.NET採用其執行緒池執行緒之一,並將其賦予這一請求。因為它寫同步,則請求處理常式將同步調用外部資源。這阻止請求執行緒,直到對外部資源的調用返回。圖 1 說明了與兩個執行緒,執行緒池,其中之一阻止等待外部的資源。

同步等待外部資源
圖 1 同步等待外部資源

最終,那外部資源調用返回,並且請求執行緒該請求。當請求已完成且準備發送回應時,請求執行緒返回到執行緒池。

這是很好 — — 直到你ASP.NET伺服器獲取更多的請求比執行緒來處理。在這一點上,額外的請求必須等待中的執行緒可用之前,他們可以運行。圖 2 說明了相同的兩個執行緒伺服器時它接收三個請求。

兩個執行緒伺服器接收三個請求
圖 2 兩個執行緒伺服器接收三個請求

在這種情況,從執行緒池執行緒指派首兩項要求。每個這些請求調用外部資源,阻止它們的執行緒。第三個請求必須等待可用執行緒之前甚至開始處理中,但該請求已在系統中。去了其計時器,而且它處於危險之中的 HTTP 錯誤 503 (服務不可用)。

但想想這一秒:這三項要求正在等待的執行緒,當有兩個其他執行緒在系統中有效地什麼都不做。這些執行緒只被阻止等待外部調用返回。他們不做任何實際的工作 ; 他們不是在運行狀態下,並不賦予任何 CPU 時間。這些執行緒正在只是被浪費掉,而需要的請求。這是通過非同步請求處理的情況。

非同步請求處理常式的操作方式不同。當一個請求時,ASP.NET採用其執行緒池執行緒之一,並將其賦予這一請求。這一次的請求處理常式將非同步調用的外部資源。這會請求執行緒返回到執行緒池直至對外部資源的調用返回。圖 3 說明了具有兩個執行緒的執行緒池,而請求非同步等待外部資源。

非同步等待外部資源
圖 3 非同步等待外部資源

重要的區別在於請求執行緒的非同步調用正在進行時的返回執行緒池。線上程池中的執行緒時,它已不再與該請求關聯。這一次,當外部資源的調用返回時,ASP.NET採用其執行緒池執行緒之一和抽調到這一要求。該執行緒會繼續處理請求。當完成請求時,執行緒再次返回到執行緒池。請注意,與同步處理常式相同的執行緒用於請求的存留期 ; 非同步處理,與此相反的是,不同的執行緒可能被分配到相同的請求 (在不同的時間)。

現在,要是來了三個請求,伺服器可以輕鬆地應付。因為執行緒被釋放到執行緒池,每當請求有它正在等待的非同步工作,他們可以自由地處理新的請求,以及現有的。非同步請求允許較小數量的執行緒來處理大量的請求。因此,對ASP.NET非同步代碼的主要優點是可擴充性。

為什麼不增加執行緒池的大小?

在這一點上,一個問題是總是問:為什麼不只是增加執行緒池的大小?答案是雙重的:非同步代碼鱗片都進一步和比阻塞執行緒池執行緒更快。

非同步代碼可以規模進一步比阻塞執行緒,因為它使用較少的記憶體 ; 在現代作業系統上的每個執行緒池執行緒有 1 MB 的堆疊,再加上 unpageable 內核堆疊。那聽起來不像很多直到你開始得到一大堆的執行緒在您的伺服器上。與此相反的是,非同步作業的記憶體開銷小得多。因此,具有非同步作業的請求具有更少記憶體壓力比與被阻塞的執行緒的請求。非同步代碼允許您使用更多的你的記憶,其他的事情 (例如,緩存)。

非同步代碼可以縮放比阻塞的執行緒更快因為執行緒池具有有限的注射速率。在撰寫本文時,率是一個執行緒每隔兩秒鐘。此注射速率極限是一件好事 ; 它避免了恒定的執行緒構造和析構。然而,考慮請求突發洪水來的時候,會發生什麼。 同步代碼可以很容易陷入請求用完所有可用的執行緒以及剩餘的請求必須等待要注入新執行緒的執行緒池。另一方面,非同步代碼並不需要像這樣 ; 限制 它是"永不中斷"如此說話。非同步代碼是更能回應請求卷突然波動的影響。

銘記非同步代碼不能代替執行緒池。這不是執行緒池或非同步代碼 ; 它的執行緒池和非同步代碼。非同步代碼允許您的應用程式的執行緒池的最佳利用。它以現有的執行緒池,並使它轉動最多為 11。

執行緒非同步工作怎麼樣?

我被問這個問題的時間。言下之意是必須有一些執行緒阻塞 I/O 調用外部資源上的地方。因此,非同步代碼釋放請求的執行緒,但只有另一個執行緒在其他地方在系統中,正確嗎?不,一點也不。

要理解為什麼非同步請求的規模,我會跟蹤非同步 I/O 調用的 (簡體) 的示例。比方說一個請求需要寫入一個檔。請求執行緒調用非同步寫入方法。WriteAsync 實現由基礎類類庫 (BCL) 中,並使用其非同步 I/O 完成埠。 因此,WriteAsync 調用被傳遞到 OS 作為非同步檔寫入。作業系統然後與驅動程式堆疊,傳遞要寫入的 I/O 請求包 (IRP) 的資料通信。

這是事情有趣的地方:如果一個裝置驅動程式不能立即處理的 IRP,它必須以非同步方式處理它。因此,司機告訴磁片開始寫作,"掛起"將回應返回給作業系統。作業系統將傳遞到"待審"BCL 和 BCL 回應不完整將任務返回到請求處理代碼。請求處理代碼等待的任務,等等從該方法返回未完成的任務。最後,請求處理代碼都是未完成的任務回到ASP.NET,並請求執行緒被釋放,以返回到執行緒池。

現在,考慮系統的目前狀態。有各種已經分配了 (例如,任務實例和 IRP) 的 I/O 結構,它們都處於掛起的完整狀態。然而,那裡是沒有線程被阻止,等待,寫操作完成。ASP.NET,BCL,也不是作業系統,無論是裝置驅動程式有致力於非同步工作執行緒。

當磁片完成寫入資料時,它會通知其通過中斷的驅動程式。該驅動程式通知已完成 IRP,OS 和 OS 通知通過完成埠 BCL。執行緒池執行緒回應該通知所完成的任務,從 WriteAsync ; 返回了 這反過來將恢復該非同步請求的代碼。有幾個執行緒"借"額很短的時間完成通知期的情況下,但沒有線程實際上被阻止在寫操作過程中。

大幅簡化本示例,但它獲取跨主要點:沒有線程所需的真正的非同步工作。沒有任何 CPU 時間有必要實際推出位元組為單位)。也是一個次要的教訓,學會。想瞭解裝置驅動程式的世界,一個裝置驅動程式必須要麼處理方式 IRP 立即或非同步。同步處理不是一個選項。在裝置驅動程式的級別,所有非平凡的 i/o 操作是非同步。許多開發商有一個對待"自然 API"的 I/O 操作是同步的這與作為建立在自然、 同步 API 層的非同步 API 的心智模型。然而,這是完全落後的:事實上,天然的 API 是非同步 ; 而且它是使用非同步 I/O 實現同步 Api !

為什麼已經沒有非同步處理常式?

如果非同步請求處理就是這麼奇妙,為什麼是不是已經可用?其實,非同步代碼是那麼好ASP.NET平臺已經從 Microsoft.NET 框架非常一開始支援非同步處理常式和模組的可擴充性。在ASP.NET2.0 中,介紹了非同步 Web 頁和 MVC 了非同步控制器ASP.NETMVC 2 中。

直到最近,然而,非同步代碼一直是難編寫和維護困難的人。許多公司決定很容易在周圍只同步開發代碼並支付更大的伺服器農場或更昂貴的託管。現在,發生了逆轉:在ASP.NET4.5,非同步使用非同步代碼和等待是幾乎一樣容易編寫同步代碼。隨著大型系統進入雲託管的和更多的規模變得越來越需求公司正在擁抱非同步和ASP.NET上等待著。

非同步代碼不是一顆銀子彈

非同步請求處理是很棒,它不會解決你所有的問題。有幾個常見的誤解,周圍什麼非同步等待著在ASP.NET可以做。

當一些開發人員瞭解非同步和等待時,他們認為它是"屈服"于用戶端 (例如,瀏覽器) 的伺服器代碼的方式。然而,非同步等待對ASP.NET唯一"產量"到ASP.NET運行時 ; HTTP 協定保持不變,和你還有只有一個回應每個請求。如果你需要那麼 SignalR 或 AJAX UpdatePanel 等待非同步/之前,你仍然需要那麼 SignalR 或 AJAX UpdatePanel 等待非同步/之後。

非同步處理與非同步請求並等待可以説明您的應用程式擴展。然而,這縮放單個伺服器 ; 你可能仍然需要計畫擴大規模。如果您需要擴展的架構,你仍然需要考慮無國籍、 冪等請求和可靠的排隊。非同步等待做有所説明:它們使您能夠充分利用您的伺服器資源,因此您不會必須經常向外擴展。但如果你確實需要向外擴展,你需要適當的分散式體系結構。

非同步等待ASP.NET.net 是所有關于 I/O。 他們真的很擅長讀和寫檔、 資料庫記錄和其他 Api。然而,它們不適合 CPU 綁定的任務。你可以踢掉一些背景性工作,通過等待 Task.Run,但這樣做沒有意義。事實上,那實際上會傷害你的可擴充性通過干擾的ASP.NET執行緒池試探法。如果你有 CPU 綁定的工作要做ASP.NET,你最好的賭注是只執行它直接對請求執行緒。作為一般規則,不要排隊到執行緒池上ASP.NET工作。

最後,考慮一下你的系統作為一個整體的可擴充性。十年前,一個常見的體系結構是要談到一個SQL Server資料庫後端的一台ASP.NETWeb 服務器。在那種簡單的體系結構中,資料庫伺服器通常是可伸縮性瓶頸,而不是 Web 服務器。使您的資料庫調用非同步大概不會説明 ; 您當然可以使用它們來擴展 Web 服務器上,但資料庫伺服器將阻止系統作為一個整體縮放。

裡克 · 安德森使非同步資料庫調用的案子在他優秀的博客,"我的資料庫調用應該是非同步嗎?"(bit.ly/1rw66UB)。有支援此功能的兩個參數:第一,非同步代碼會很困難 (和因此昂貴的開發人員的時間相比,只購買較大的伺服器) ; 而且第二,縮放 Web 服務器毫無道理如果資料庫後端是瓶頸。兩那些論據作出完美的感覺,當那個帖子寫的但隨著時間的推移削弱了這兩個論點。第一,非同步代碼是用非同步寫並等待要容易得多。第二,為網站的後端資料縮放作為世界移動到雲計算。現代背結束如微軟 Azure SQL 資料庫,NoSQL 和其他 Api 可以規模遠比單個SQL Server,向瓶頸後推到 Web 服務器。在這種情況下,等待非同步/可以通過縮放ASP.NET帶來巨大的利益。

在開始之前

首先你需要先知道該非同步,等待只支援對ASP.NET4.5。還有 NuGet 套裝程式稱為 Microsoft.Bcl.Async,使非同步和.NET 框架 4,在等待著,但不是使用它 ; 它將無法正常工作 !原因是ASP.NET本身不得不改變它管理其非同步請求處理,以更好地使用非同步和等待 ; NuGet 套裝程式包含編譯器需要,但不是會修補程式ASP.NET運行庫的所有類型。尚無解決方法 ; 你需要ASP.NET4.5 或更高。

接下來,要知道ASP.NET4.5 介紹"怪癖模式"在伺服器上。如果您創建一個新的ASP.NET4.5 專案,你不必擔心。然而,如果你將一個現有的專案升級到ASP.NET4.5,怪癖是所有打開的。我建議您將它們全部關閉編輯你的 web.config 並將 HTTPRuntime.targetFramework 設置為 4.5。如果您的應用程式將失敗,並此設置 (和你不想花時間去修改它),你至少可以等待非同步/工作通過添加 appSetting 鍵的 aspnet:UseTaskFriendlySynchronizationCoNtext 值"true"。AppSetting 金鑰是不必要的如果你有 HTTPRuntime.targetFramework 設置為 4.5。Web 開發團隊在這種新的"怪癖模式"的細節通過博客 bit.ly/1pbmnzK。秘訣:如果你看到奇怪的行為或異常,並且您呼叫堆疊包括 LegacyAspNetSynchronizationCoNtext,您的應用程式正在運行在這種怪癖模式。LegacyAspNet­SynchronizationCoNtext 不相容與非同步 ; 你需要週期性 AspNetSynchronizationCoNtextASP.NET4.5。

在ASP.NET4.5 中,ASP.NET的所有設置都有很好的預設值為非同步請求,但有幾個你可能想要更改其他設置。第一個是 IIS 設置:考慮提高 IIS/HTTP.sys 佇列限制 (應用程式池 |高級設置 |佇列長度) 從 1,000 到 5000 的預設值。 另一種是.NET 運行時設置:ServicePointManager.DefaultConnectionLimit,具有 12 倍的內核數的預設值。DefaultConnectionLimit 限制數量的併發傳出連接到相同的主機名稱。

一句話就中止請求

當ASP.NET同步處理一個請求時,它有一個非常簡單的機制,為中止請求 (例如,如果請求超出了其超時):它將中止為該請求的執行緒。在同步的世界中,每個請求有相同的工作執行緒從開始到結束,這有意義。中止執行緒並不是精彩的 AppDomain,長期穩定,所以在預設情況下ASP.NET將定期回收您的應用程式保持乾淨的東西了。

與非同步請求,ASP.NET不會中止執行緒,如果它想要中止請求。相反,它將取消使用 CancellationToken 的要求。非同步請求處理常式應該接受並尊重解除標記。大多數較新的框架 (包括 Web API、 MVC 和那麼 SignalR) 將構造和傳遞你的 CancellationToken 直接 ; 你要做的就是將它聲明為參數。您還可以訪問ASP.NET權杖直接 ; 例如,HttpRequest.TimedOutToken 是 CancellationToken,取消時請求超時。

隨著應用程式移動到雲計算,中止請求變得更重要。基於雲的應用程式是時間的更依賴于外部的服務,可以有任意數量。例如,一個標準的模式是重試外部請求與指數退避演算法 ; 如果您的應用程式依賴于這樣的多個服務,它是一個好的主意,要應用一個超時上限為您加工為一體的請求。

目前狀態的非同步支援

許多圖書館已更新為與非同步相容性。在版本 6 中,非同步支援被添加到Entity Framework(EntityFramework NuGet 套裝程式) 中。 你必須小心,以避免延遲載入,當工作以非同步方式,不過,因為延遲載入始終同步執行。HttpClient (在 Microsoft.Net.Http NuGet 套裝程式) 是現代的 HTTP 用戶端設計與非同步在腦海裡,理想的調用外部其他 Api ; 它是一個現代的替換區域性和 WebClient。微軟 Azure 存儲用戶端庫 (在 WindowsAzure.Storage NuGet 套裝程式) 版本 2.1 中添加非同步支援。

新的框架,例如 Web API 和那麼 SignalR 全力支援非同步和等待。尤其是 web API 建立了圍繞非同步支援其整個管線:不僅非同步控制器,但非同步篩選器和處理常式中,太。Web API,那麼 SignalR 有一個很自然的非同步故事:你可以"只是做它",它"只是工作。"

這給我們帶來一個悲傷的故事:今天,ASP.NETMVC 只是部分支援非同步和等待。基本支援有 — — 非同步控制器操作和取消適當工作。ASP.NETWeb 網站對如何在ASP.NETMVC 中使用非同步控制器操作絕對優秀的教程 (bit.ly/1m1LXTx) ; 它是入門非同步對 MVC 的最佳資源。不幸的是,ASP.NETMVC 不 (目前) 支援非同步篩選器 (bit.ly/1oAyHLc) 或非同步兒童行動 (bit.ly/1px47RG)。

ASP.NETWeb 表單是一個舊的框架,但它也有足夠的支援,為非同步和等待。入門的最佳資源,又為非同步 Web 表單上ASP.NETWeb 網站教程 (bit.ly/Ydho7W)。與 Web 表單,非同步支援是可選的。 您必須首先將 Page.Async 設置為 true,那麼你可以使用 PageAsyncTask 來與該頁面註冊非同步工作 (或者,您可以使用非同步 void 的事件處理常式)。PageAsyncTask 還支援取消。

如果您有一個自訂 HTTP 處理常式或 HTTP 模組,ASP.NET現在支援非同步版本的那些人,以及。HTTP 處理常式通過 HttpTaskAsyncHandler 支援 (bit.ly/1nWpWFj) 和 HTTP 模組支援通過 EventHandlerTaskAsyncHelper (bit.ly/1m1Sn4O)。

在撰寫本文時,ASP.NET團隊正在對一個新的專案,稱為ASP.NETvNext。在 vNext,則整個管線是非同步預設情況下。目前,該計畫是將 MVC 及 Web API 合併為一個單一的框架具有充分支援的/等待非同步 (包括非同步篩檢程式和非同步查看元件)。其他非同步準備框架如那麼 SignalR 將會在 vNext 找到一個自然的家。真的,未來是非同步。

尊重安全網

ASP.NET4.5 介紹幾個新"安全網",説明您在您的應用程式中捕捉非同步問題。這些預設情況下,並應繼續留任。

當同步處理常式試圖執行非同步工作時,你會載入並顯示消息,"不能在這個時候開始一個非同步作業"。有兩個主要原因為此異常。第一個是當一個 Web 表單頁具有非同步事件處理常式,但被忽視,將 Page.Async 設置為 true。第二個是當同步代碼調用非同步 void 方法。這是為了避免非同步 void 的另一個原因。

其他安全網是非同步處理常式:當非同步處理常式完成該請求,但是ASP.NET檢測到未完成的非同步工作時,你會得到一個不正確­OperationException 並顯示消息"非同步模組或處理常式完成非同步作業時仍然掛起。"這通常是由於非同步代碼調用非同步 void 方法,但也能引起不當使用的事件架構非同步模式 (EAP) 的一個組成部分 (bit.ly/19VdUWu)。

還有一個選項,您可以使用關閉這兩個安全網:HttpCoNtext.AllowAsyncDuringSyncStages (它也可以設置在 web.config 中)。在網際網路上的幾頁建議此設置,每當你看到這些例外情況。我不能更強烈地不同意。說真的,我不知道為什麼這是甚至可能。禁用安全網是一個可怕的想法。我能想到的唯一可能原因是您的代碼是否已做一些非常先進的非同步東西 (超出任何遇到過),和你是一個多執行緒的天才。所以,如果你讀過這整篇文章打呵欠和思維,"求你了,我沒有 n00b,"然後我想,可以考慮禁用安全網。我們的餘生,這是極其危險的選項,除非你充分意識到後果,不應設置。

使用者入門

終於 !準備好開始利用非同步和等待嗎?我感謝您的耐心。

首先,查看這篇文章,以確保等待非同步/是有益於您的體系結構中的"非同步代碼是不銀色子彈"節。下一步,更新您的應用程式到ASP.NET4.5 和關閉怪癖模式 (它不是一個壞的主意,運行它在這一點只是為了確保沒有破壞)。在這一點上,你準備好開始真正非同步等待工作。

開始在"葉子"。想想您的請求處理標識任何 O 基礎的操作,特別是任何基於網路的。常見的例子是資料庫查詢和命令以及對其他 Web 服務和 Api 的調用。開始時,選擇之一,做一點研究,以找到用於執行該操作,使用非同步/等待最好的選擇。許多內置的 BCL 類型現在是非同步準備在.NET 框架 4.5 ; 例如,郵件具有與 SendMailAsync 的方法。某些類型具有非同步準備更換可用 ; 例如,區域性和 WebClient 可以更換為 HttpClient。升級您的庫版本,如果有必要的 ; 例如,Entity Framework得到非同步相容方法在 EF6。

然而,避免在庫中的"假非同步"。假的非同步性是當一個元件具有非同步準備 API,但它由只環繞線上程池執行緒的同步 API 實現。這是利於上ASP.NET的可伸縮性。 假非同步運動的一個突出的例子是 Newtonsoft JSON.NET,否則為優秀的圖書館。最好是不要求的 (假) 的非同步版本序列化 JSON ; 只是改用同步版本。一個更複雜的例子,假非同步是 BCL 檔流。當打開一個檔流時,它必須顯式打開為非同步訪問 ; 否則,它將使用假非同步同步阻塞執行緒池執行緒上的檔讀取和寫入。

一旦你選擇了"葉",然後從代碼中調用該 API 的方法開始,並把它變成一種調用非同步準備 API 通過等待的非同步方法。如果您正在調用的 API 支援 CancellationToken,您的方法應採取 CancellationToken,把它傳遞給 API 方法。

每當您標記方法非同步,您應該更改其返回的類型:無效成為任務,而非 void 類型 T 成為任務 < T >。你會發現,然後所有的這種方法的調用方需要成為非同步,所以他們可以等待任務,等等。此外,將非同步追加到您的方法,以遵循基於任務的非同步模式的慣例的名稱 (bit.ly/1uBKGKR)。

允許的等待非同步/模式,長大了你呼叫堆疊向"主幹"。在樹幹上,在您的代碼將與ASP.NET框架 (MVC,Web 表單、 Web API) 介面。閱讀這篇文章,你的框架結合非同步代碼前面的"非同步支援目前狀態"部分中的適當教程。

一路走來,查明任何執行緒本地狀態。因為非同步請求可能會更改執行緒,執行緒本地狀態,如 ThreadStaticAttribute,ThreadLocal < T >、 執行緒資料槽和 CallCoNtext.GetData/SetData 將不工作。替換這些與 HttpCoNtext.Items,如果可能的話 ; 或者你可以在 CallCoNtext.LogicalGetData/LogicalSetData 中存儲不可變的資料。

下面是我發現了有用的提示:(暫時的),您可以複製您的代碼以創建垂直分區。利用這種技術,你不改變你同步的方法對非同步 ; 您將複製整個同步方法,然後更改將非同步副本。你可以然後把大部分的應用程式使用的同步方法,只需創建一個垂直切片的非同步。這是偉大的如果你想要探討作為一個概念證明的非同步或做負載測試上要得到一種感覺為您的系統可能如何擴展的應用程式只是一部分。你可以有一個請求 (或頁面),是完全非同步而您的應用程式的其餘部分保持同步。當然,你不想要重複每一個你的方法 ; 最終,O 綁定的所有代碼都將非同步和同步複本可以都被刪除。

總結

我希望這篇文章可以説明你的概念基礎上ASP.NET的非同步請求。 使用非同步等待很容易比以往編寫 Web 應用程式、 服務和最大限度的 Api 使用的伺服器資源。非同步太棒了 !


Stephen Cleary 是丈夫、 父親和住在北密歇根的程式師。他曾與多執行緒和非同步程式設計的 16 年裡和自在 Microsoft.NET 框架以來第一次社區技術預覽使用非同步支援。他的主頁,包括他的博客,是在 stephencleary.com

感謝以下的微軟技術專家對本文的審閱:James McCaffrey