網址:
通過使用異步編程,你可以避免性能瓶頸並增強應用程序的總體響應能力。 但是,編寫異步應用程序的傳統技術可能比較復雜,使它們難以編寫、調試和維護。
C# 5 引入了一種簡便方法,即異步編程。此方法利用了 .NET Framework 4.5 及更高版本、.NET Core 和 Windows 運行時中的異步支持。 編譯器可執行開發人員曾進行的高難度工作,且應用程序保留了一個類似於同步代碼的邏輯結構。 因此,你只需做一小部分工作就可以獲得異步編程的所有好處。
本主題概述了何時以及如何使用異步編程,並包括指向包含詳細信息和示例的支持主題的鏈接。
異步編程提升響應能力
異步對可能會被屏蔽的活動(如 Web 訪問)至關重要。 對 Web 資源的訪問有時很慢或會延遲。 如果此類活動在同步過程中被屏蔽,整個應用必須等待。 在異步過程中,應用程序可繼續執行不依賴 Web 資源的其他工作,直至潛在阻止任務完成。
下表顯示了異步編程提高響應能力的典型區域。 列出的 .NET 和 Windows 運行時 API 包含支持異步編程的方法。
應用程序區域 | 包含異步方法的 .NET 類型 | 包含異步方法的 Windows 運行時類型 |
---|---|---|
Web 訪問 | HttpClient | SyndicationClient |
使用文件 | StreamWriter, StreamReader, XmlReader | StorageFile |
使用圖像 | MediaCapture、BitmapEncoder、BitmapDecoder | |
WCF 編程 | 同步和異步操作 |
由於所有與用戶界面相關的活動通常共享一個線程,因此,異步對訪問 UI 線程的應用程序來說尤為重要。 如果任何進程在同步應用程序中受阻,則所有進程都將受阻。 你的應用程序停止響應,因此,你可能在其等待過程中認為它已經失敗。
使用異步方法時,應用程序將繼續響應 UI。 例如,你可以調整窗口的大小或最小化窗口;如果你不希望等待應用程序結束,則可以將其關閉。
當設計異步操作時,該基於異步的方法將自動傳輸的等效對象添加到可從中選擇的選項列表中。 開發人員只需要投入較少的工作量即可使你獲取傳統異步編程的所有優點。
異步方法更容易編寫
C# 中的 Async 和 Await 關鍵字是異步編程的核心。 通過這兩個關鍵字,可以使用 .NET Framework、.NET Core 或 Windows 運行時中的資源,輕松創建異步方法(幾乎與創建同步方法一樣輕松)。 使用 async
和 await
定義的異步方法簡稱為“異步 (Async) 方法”。
下面的示例演示了一種異步方法。 你應對代碼中的幾乎所有內容都非常熟悉。 注釋調出你添加的用來創建異步的功能。
此主題的末尾提供完整的 Windows Presentation Foundation (WPF) 示例文件,請從異步示例:“使用 Async 和 Await 的異步編程”示例下載此示例。
// Three things to note in the signature: // - The method has an async modifier. // - The return type is Task or Task<T>. (See "Return Types" section.) // Here, it is Task<int> because the return statement returns an integer. // - The method name ends in "Async." async Task<int> AccessTheWebAsync() { // You need to add a reference to System.Net.Http to declare client. HttpClient client = new HttpClient(); // GetStringAsync returns a Task<string>. That means that when you await the // task you'll get a string (urlContents). Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com"); // You can do work here that doesn't rely on the string from GetStringAsync. DoIndependentWork(); // The await operator suspends AccessTheWebAsync. // - AccessTheWebAsync can't continue until getStringTask is complete. // - Meanwhile, control returns to the caller of AccessTheWebAsync. // - Control resumes here when getStringTask is complete. // - The await operator then retrieves the string result from getStringTask. string urlContents = await getStringTask; // The return statement specifies an integer result. // Any methods that are awaiting AccessTheWebAsync retrieve the length value. return urlContents.Length; }
如果 AccessTheWebAsync
在調用 GetStringAsync
和等待其完成期間不能進行任何工作,則你可以通過在下面的單個語句中調用和等待來簡化代碼。
string urlContents = await client.GetStringAsync();
以下特征總結了使上一個示例成為異步方法的原因。
-
方法簽名包含
async
修飾符。 -
按照約定,異步方法的名稱以“Async”后綴結尾。
-
返回類型為下列類型之一:
-
如果你的方法有操作數為 TResult 類型的返回語句,則為 Task<TResult>。
-
如果你的方法沒有返回語句或具有沒有操作數的返回語句,則為 Task。
-
Void
:如果要編寫異步事件處理程序。 -
包含
GetAwaiter
方法的其他任何類型(自 C# 7 起)。
有關詳細信息,請參見本主題后面的“返回類型和參數”。
-
-
方法通常包含至少一個 await 表達式,該表達式標記一個點,在該點上,直到等待的異步操作完成方法才能繼續。 同時,將方法掛起,並且控件返回到方法的調用方。 本主題的下一節將解釋懸掛點發生的情況。
在異步方法中,可使用提供的關鍵字和類型來指示需要完成的操作,且編譯器會完成其余操作,其中包括持續跟蹤控件以掛起方法返回等待點時發生的情況。 一些常規流程(例如,循環和異常處理)在傳統異步代碼中處理起來可能很困難。 在異步方法中,元素的編寫頻率與同步解決方案相同且此問題得到解決。
若要詳細了解舊版 .NET Framework 中的異步性,請參閱 TPL 和傳統 .NET Framework 異步編程。
異步方法的運行機制
異步編程中最需弄清的是控制流是如何從方法移動到方法的。 下圖可引導你完成該過程。
關系圖中的數值對應於以下步驟。
-
事件處理程序調用並等待
AccessTheWebAsync
異步方法。 -
AccessTheWebAsync
可創建 HttpClient 實例並調用 GetStringAsync 異步方法以下載網站內容作為字符串。 -
GetStringAsync
中發生了某種情況,該情況掛起了它的進程。 可能必須等待網站下載或一些其他阻止活動。 為避免阻止資源,GetStringAsync
會將控制權出讓給其調用方AccessTheWebAsync
。GetStringAsync
返回 Task<TResult>,其中TResult
為字符串,並且AccessTheWebAsync
將任務分配給getStringTask
變量。 該任務表示調用GetStringAsync
的正在進行的進程,其中承諾當工作完成時產生實際字符串值。 -
由於尚未等待
getStringTask
,因此,AccessTheWebAsync
可以繼續執行不依賴於GetStringAsync
得出的最終結果的其他工作。 該任務由對同步方法DoIndependentWork
的調用表示。 -
DoIndependentWork
是完成其工作並返回其調用方的同步方法。 -
AccessTheWebAsync
已用完工作,可以不受getStringTask
的結果影響。 接下來,AccessTheWebAsync
需要計算並返回該下載字符串的長度,但該方法僅在具有字符串時才能計算該值。因此,
AccessTheWebAsync
使用一個 await 運算符來掛起其進度,並把控制權交給調用AccessTheWebAsync
的方法。AccessTheWebAsync
將Task<int>
返回給調用方。 該任務表示對產生下載字符串長度的整數結果的一個承諾。備注
如果
GetStringAsync
(因此getStringTask
)在AccessTheWebAsync
等待前完成,則控件會保留在AccessTheWebAsync
中。 如果異步調用過程 (AccessTheWebAsync
) 已完成,並且 AccessTheWebSync 不必等待最終結果,則掛起然后返回到getStringTask
將造成成本浪費。在調用方內部(此示例中的事件處理程序),處理模式將繼續。 在等待結果前,調用方可以開展不依賴於
AccessTheWebAsync
結果的其他工作,否則就需等待片刻。 事件處理程序等待AccessTheWebAsync
,而AccessTheWebAsync
等待GetStringAsync
。 -
GetStringAsync
完成並生成一個字符串結果。 字符串結果不是通過按你預期的方式調用GetStringAsync
所返回的。 (記住,該方法已返回步驟 3 中的一個任務)。相反,字符串結果存儲在表示getStringTask
方法完成的任務中。 await 運算符從getStringTask
中檢索結果。 賦值語句將檢索到的結果賦給urlContents
。 -
當
AccessTheWebAsync
具有字符串結果時,該方法可以計算字符串長度。 然后,AccessTheWebAsync
工作也將完成,並且等待事件處理程序可繼續使用。 在此主題結尾處的完整示例中,可確認事件處理程序檢索並打印長度結果的值。
如果你不熟悉異步編程,請花 1 分鍾時間考慮同步行為和異步行為之間的差異。 當其工作完成時(第 5 步)會返回一個同步方法,但當其工作掛起時(第 3 步和第 6 步),異步方法會返回一個任務值。 在異步方法最終完成其工作時,任務會標記為已完成,而結果(如果有)將存儲在任務中。
若要詳細了解控制流,請參閱異步程序中的控制流 (C#)。
API 異步方法
你可能想知道從何處可以找到 GetStringAsync
等支持異步編程的方法。 .NET Framework 4.5 或更高版本以及 .NET Core 包含許多可與 async
和 await
結合使用的成員。 可以通過追加到成員名稱的“Async”后綴和 Task 或 Task<TResult> 的返回類型,識別這些成員。 例如,System.IO.Stream
類包含 CopyToAsync、ReadAsync 和 WriteAsync 等方法,以及同步方法 CopyTo、Read 和 Write。
Windows 運行時也包含許多可以在 Windows 應用中與 async
和 await
結合使用的方法。 有關詳細信息和示例方法,請參閱快速入門:使用 await 運算符進行異步編程、異步編程(Windows 應用商店應用)和 WhenAny:.NET Framework 和 Windows 運行時之間的橋接。
線程
異步方法旨在成為非阻止操作。 異步方法中的 await
表達式在等待的任務正在運行時不會阻止當前線程。 相反,表達式在繼續時注冊方法的其余部分並將控件返回到異步方法的調用方。
async
和 await
關鍵字不會創建其他線程。 因為異步方法不會在其自身線程上運行,因此它不需要多線程。 只有當方法處於活動狀態時,該方法將在當前同步上下文中運行並使用線程上的時間。 可以使用 Task.Run 將占用大量 CPU 的工作移到后台線程,但是后台線程不會幫助正在等待結果的進程變為可用狀態。
對於異步編程而言,該基於異步的方法優於幾乎每個用例中的現有方法。 具體而言,此方法比 BackgroundWorker 類更適用於 IO 綁定操作,因為此代碼更簡單且無需防止爭用條件。 結合 Task.Run 方法使用時,異步編程比 BackgroundWorker 更適用於 CPU 綁定操作,因為異步編程將運行代碼的協調細節與 Task.Run
傳輸至線程池的工作區分開來。
async 和 await
如果使用 async 修飾符將某種方法指定為異步方法,即啟用以下兩種功能。
-
標記的異步方法可以使用 await 來指定暫停點。 await 運算符通知編譯器異步方法只有直到等待的異步過程完成才能繼續通過該點。 同時,控件返回至異步方法的調用方。
異步方法在
await
表達式執行時暫停並不構成方法退出,只會導致finally
代碼塊不運行。 -
標記的異步方法本身可以通過調用它的方法等待。
異步方法通常包含 await
運算符的一個或多個實例,但缺少 await
表達式也不會導致生成編譯器錯誤。 如果異步方法未使用 await
運算符標記暫停點,那么異步方法會作為同步方法執行,即使有 async
修飾符,也不例外。 編譯器將為此類方法發布一個警告。
async
和 await
都是上下文關鍵字。 有關更多信息和示例,請參見以下主題:
返回類型和參數
異步方法通常返回 Task 或 Task<TResult>。 在異步方法中,await
運算符應用於通過調用另一個異步方法返回的任務。
如果方法包含指定 TResult
類型操作數的 return 語句,將 Task<TResult> 指定為返回類型。
如果方法不含任何 return 語句或包含不返回操作數的 return 語句,將 Task 用作返回類型。
自 C# 7 起,還可以指定其他任何返回類型,前提是返回類型包含 GetAwaiter
方法。 例如,ValueTask<TResult> 就是這種類型。 可用於 System.Threading.Tasks.Extension NuGet 包。
下面的示例演示如何聲明並調用可返回 Task<TResult> 或 Task 的方法。
// Signature specifies Task<TResult> async Task<int> TaskOfTResult_MethodAsync() { int hours; // . . . // Return statement specifies an integer result. return hours; } // Calls to TaskOfTResult_MethodAsync Task<int> returnedTaskTResult = TaskOfTResult_MethodAsync(); int intResult = await returnedTaskTResult; // or, in a single statement int intResult = await TaskOfTResult_MethodAsync(); // Signature specifies Task async Task Task_MethodAsync() { // . . . // The method has no return statement. } // Calls to Task_MethodAsync Task returnedTask = Task_MethodAsync(); await returnedTask; // or, in a single statement await Task_MethodAsync();
每個返回的任務表示正在進行的工作。 任務可封裝有關異步進程狀態的信息,如果未成功,則最后會封裝來自進程的最終結果或進程引發的異常。
異步方法也可以具有 void
返回類型。 該返回類型主要用於定義需要 void
返回類型的事件處理程序。 異步事件處理程序通常用作異步程序的起始點。
無法等待具有 void
返回類型的異步方法,並且無效返回方法的調用方捕獲不到異步方法拋出的任何異常。
異步方法無法聲明 ref 或 out 參數,但可以調用包含此類參數的方法。 同樣,異步方法無法通過引用返回值,但可以調用包含 ref 返回值的方法。
有關詳細信息和示例,請參閱異步返回類型 (C#)。 若要詳細了解如何在異步方法中捕獲異常,請參閱 try-catch。
Windows 運行時編程中的異步 API 具有下列返回類型之一(類似於任務):
-
對應 Task 的 IAsyncAction
有關詳細信息和示例,請參閱快速入門:使用 await 運算符進行異步編程。
命名約定
按照約定,將“Async”追加到包含 async
修飾符的方法的名稱中。
如果某一約定中的事件、基類或接口協定建議其他名稱,則可以忽略此約定。 例如,你不應重命名常用事件處理程序,例如 Button1_Click
。
相關主題和示例 (Visual Studio)
標題 | 描述 | 示例 |
---|---|---|
演練:使用 Async 和 Await 訪問 Web (C#) | 演示如何將一個同步 WPF 解決方案轉換成一個異步 WPF 解決方案。 應用程序下載一系列網站。 | 異步示例:“訪問 Web”演練 |
如何:使用 Task.WhenAll 擴展異步演練 (C#) | 將 Task.WhenAll 添加到上一個演練。 使用 WhenAll 同時啟動所有下載。 |
|
如何:使用 Async 和 Await 並行發出多個 Web 請求 (C#) | 演示如何同時開始幾個任務。 | 異步示例:並行發出多個 Web 請求 |
異步返回類型 (C#) | 描述異步方法可返回的類型,並解釋每種類型適用於的情況。 | |
異步程序中的控制流 (C#) | 通過異步程序中的一系列 await 表達式來詳細跟蹤控制流。 | 異步示例:異步程序中的控制流 |
微調異步應用程序 (C#) | 演示如何將以下功能添加到異步解決方案: - 取消一個異步任務或一組任務(C#) - 在一段時間后取消異步任務 (C#) - 在完成一個異步任務后取消剩余任務 (C#) - 啟動多個異步任務並在其完成時進行處理 (C#) |
異步示例:微調應用程序 |
在異步應用程序中處理重入 (C#) | 演示如何處理有效的異步操作在運行時重啟的情況。 | |
WhenAny:.NET Framework 和 Windows 運行時之間的橋接 | 展示了如何橋接 .NET Framework 與 Windows 運行時中 IAsyncOperations 的任務類型,以便可以將 WhenAny 與 Windows 運行時 方法結合使用。 | 異步示例:橋接 .NET 和 Windows 運行時(AsTask 和 WhenAny) |
異步取消:.NET Framework 和 Windows 運行時之間的橋接 | 展示了如何橋接 .NET Framework 與 Windows 運行時中 IAsyncOperations 的任務類型,以便可以將 CancellationTokenSource 與 Windows 運行時 方法結合使用。 | 異步示例:橋接 .NET 和 Windows 運行時(AsTask 和 Cancellation) |
使用 Async 進行文件訪問 (C#) | 列出並演示使用 async 和 await 訪問文件的好處。 | |
基於任務的異步模式 (TAP) | 描述 .NET Framework 中異步的新模式。 該模式基於 Task 和 Task<TResult> 類型。 | |
Channel 9 上的異步相關視頻 | 提供指向有關異步編程的各種視頻的鏈接。 |
完整示例
下面的代碼來自於本主題介紹的 Windows Presentation Foundation (WPF) 應用程序的 MainWindow.xaml.cs 文件。 可以從異步示例:“使用 Async 和 Await 的異步編程”示例下載示例。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; // Add a using directive and a reference for System.Net.Http; using System.Net.Http; namespace AsyncFirstExample { public partial class MainWindow : Window { // Mark the event handler with async so you can use await in it. private async void StartButton_Click(object sender, RoutedEventArgs e) { // Call and await separately. //Task<int> getLengthTask = AccessTheWebAsync(); //// You can do independent work here. //int contentLength = await getLengthTask; int contentLength = await AccessTheWebAsync(); resultsTextBox.Text += String.Format("\r\nLength of the downloaded string: {0}.\r\n", contentLength); } // Three things to note in the signature: // - The method has an async modifier. // - The return type is Task or Task<T>. (See "Return Types" section.) // Here, it is Task<int> because the return statement returns an integer. // - The method name ends in "Async." async Task<int> AccessTheWebAsync() { // You need to add a reference to System.Net.Http to declare client. HttpClient client = new HttpClient(); // GetStringAsync returns a Task<string>. That means that when you await the // task you'll get a string (urlContents). Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com"); // You can do work here that doesn't rely on the string from GetStringAsync. DoIndependentWork(); // The await operator suspends AccessTheWebAsync. // - AccessTheWebAsync can't continue until getStringTask is complete. // - Meanwhile, control returns to the caller of AccessTheWebAsync. // - Control resumes here when getStringTask is complete. // - The await operator then retrieves the string result from getStringTask. string urlContents = await getStringTask; // The return statement specifies an integer result. // Any methods that are awaiting AccessTheWebAsync retrieve the length value. return urlContents.Length; } void DoIndependentWork() { resultsTextBox.Text += "Working . . . . . . .\r\n"; } } } // Sample Output: // Working . . . . . . . // Length of the downloaded string: 41564.