您可以使用異步程式設計來避免效能瓶頸,並增強應用程式的整體回應性。 不過,撰寫異步應用程式的傳統技術可能很複雜,因此難以撰寫、偵錯和維護。
C# 支援簡化的方法,即非同步程式設計,在 .NET 執行階段中使用非同步支援。 編譯程式會執行開發人員用來執行的困難工作,而您的應用程式會保留類似同步程式代碼的邏輯結構。 因此,您可以使用一小部分的努力,取得異步程序設計的所有優點。
本文提供使用非同步程式設計的時機及方式概觀,並包含包含詳細資料和範例的其他文章的連結。
Async 可改善回應性
Asynchrony 對於可能封鎖的活動而言非常重要,例如 Web 存取。 有時 Web 資源的存取速度很慢或延遲。 如果在同步進程中封鎖這類活動,整個應用程式必須等候。 在異步程式中,應用程式可以繼續執行其他不相依於 Web 資源的工作,直到潛在的封鎖工作完成為止。
下表顯示異步程序設計可改善回應性的一般區域。 .NET 和 Windows 執行時間列出的 API 包含支援異步程式設計的方法。
| 應用程式區域 | 具有異步方法的 .NET 類型 | 具有異步方法的 Windows 執行時間類型 |
|---|---|---|
| 網路存取 | HttpClient | Windows.Web.Http.HttpClient SyndicationClient |
| 處理檔案 | JsonSerializer StreamReader StreamWriter XmlReader XmlWriter |
StorageFile |
| 處理圖像 | MediaCapture BitmapEncoder BitmapDecoder |
|
| WCF 程式設計 | 同步和異步操作 |
Asynchrony 對於存取 UI 線程的應用程式來說特別有價值,因為所有 UI 相關活動通常會共用一個線程。 如果同步應用程式中有任何進程遭到封鎖,則會封鎖所有進程。 您的應用程式暫時無法回應,您可能會認為它已經失敗,其實它只是正在等待。
當您使用異步方法時,應用程式會繼續回應 UI。 例如,您可以調整或最小化視窗的大小,或者如果您不想等待應用程式完成,可以關閉應用程式。
異步方法相當於為設計異步作業時可供選擇的選項列表新增自動變速器。 也就是說,您會獲得傳統異步程序設計的所有優點,但開發人員所付出的努力要少得多。
異步函式很簡單撰寫
C# 中的 async 和 await 關鍵詞是異步程序設計的核心。 透過使用這兩個關鍵詞,您可以使用 .NET Framework、.NET Core 或 Windows 運行時間中的資源,建立異步方法,幾乎就像建立同步方法一樣容易。 您使用 async 關鍵字定義的異步 方法稱為異步方法。
以下範例,顯示非同步方法。 你應該會覺得幾乎所有程式碼都很熟悉。
您可以從 使用 async 和 await 進行 C# 中的異步程式設計 下載完整的 Windows Presentation Foundation (WPF) 範例。
public async Task<int> GetUrlContentLengthAsync()
{
using var client = new HttpClient();
Task<string> getStringTask =
client.GetStringAsync("https://learn.microsoft.com/dotnet");
DoIndependentWork();
string contents = await getStringTask;
return contents.Length;
}
void DoIndependentWork()
{
Console.WriteLine("Working...");
}
您可以從上述範例中了解數個做法。 從方法簽章開始。 其中包括async修飾詞。 傳回類型為 Task<int> (如需更多選項,請參閱一節)。 方法名稱結尾為 Async。 在方法的主體中,GetStringAsync 回傳 Task<string>。 這意味著當您 await 執行任務時,您會得到一個 string (contents)。 在等待任務之前,您可以執行不依賴string且不涉及GetStringAsync的工作。
請密切關注 await 操作員。 它會暫停 GetUrlContentLengthAsync:
-
GetUrlContentLengthAsync在完成之前getStringTask無法繼續。 - 同時,控制將傳回給呼叫
GetUrlContentLengthAsync的端。 - 當
getStringTask完成後,程序將繼續執行。 - 運算子
await接著會從string擷取getStringTask的結果。
return 語句會指定整數結果。 等候 GetUrlContentLengthAsync 的任何方法會擷取長度值。
如果 GetUrlContentLengthAsync 沒有任何可在呼叫 GetStringAsync 和等候完成之間執行的工作,您可以在下列單一語句中呼叫和等候來簡化程序代碼。
string contents = await client.GetStringAsync("https://learn.microsoft.com/dotnet");
下列特性摘要說明先前範例如何成為異步方法:
方法簽章包含
async修飾詞。依慣例,異步方法的名稱會以 「Async」 後綴結尾。
傳回型態是下列其中一種類型:
- 如果您的方法具有運算元類型為Task<TResult>的return語句,則
TResult。 - Task 如果您的方法沒有 return 語句,或 return 語句沒有操作數。
-
void如果您要撰寫非同步事件處理程式。 - 具有
GetAwaiter方法的任何其他類型。
如需詳細資訊,請參閱 傳回類型和參數 一節。
- 如果您的方法具有運算元類型為Task<TResult>的return語句,則
方法通常包含至少一個
await表達式,這標記了方法無法繼續進行,直到等待的異步操作完成的點。 同時,方法會暫停,而控件會傳回方法的呼叫端。 本文的下一節說明了暫停點發生的情況。
在異步方法中,您可以使用提供的關鍵詞和類型來指出您想要執行的動作,而編譯程式會執行其餘作業,包括追蹤當控件回到暫停方法中的等候點時,必須執行的動作。 某些例行程式,例如迴圈和例外狀況處理,在傳統異步程序代碼中可能很難處理。 在異步方法中,您可以像在同步解決方案中一樣撰寫這些元素,並解決問題。
如需舊版 .NET Framework 中異步的詳細資訊,請參閱 TPL 和傳統 .NET Framework 異步程序設計。
異步方法中會發生什麼事
異步程式開發中最重要的是要理解控制流如何在不同的方法間傳遞。 下圖引導您完成此過程:
當呼叫方法呼叫異步方法時,圖解中的數字對應下列步驟。
呼叫方法會呼叫並等候
GetUrlContentLengthAsync異步方法。GetUrlContentLengthAsync會建立 HttpClient 實例,並呼叫 GetStringAsync 異步方法,以字串的形式下載網站的內容。在
GetStringAsync發生的事件會暫停其進展。 也許它必須等待網站下載或其他封鎖活動。 若要避免封鎖資源,GetStringAsync將控制權移交給其呼叫端GetUrlContentLengthAsync。GetStringAsync會傳 Task<TResult>回 ,其中TResult是字串,並將GetUrlContentLengthAsync工作指派給getStringTask變數。 工作代表呼叫GetStringAsync的持續程序,承諾在工作完成時產生實際字串值。由於
getStringTask尚未被等待,GetUrlContentLengthAsync可以繼續進行不依賴GetStringAsync最終結果的其他工作。 該工作是由對同步方法DoIndependentWork的呼叫表示。DoIndependentWork是同步方法,可執行其工作並傳回其呼叫端。GetUrlContentLengthAsync用盡了在沒有來自getStringTask的結果時可以完成的工作。GetUrlContentLengthAsyncnext 想要計算並傳回所下載字串的長度,但方法在方法具有字串之前無法計算該值。因此,
GetUrlContentLengthAsync使用 await 運算符暫停其進度,並將控制權交予呼叫GetUrlContentLengthAsync的方法。GetUrlContentLengthAsync傳回Task<int>給呼叫者。 該任務承諾會生成一個整數結果,這是下載的字串長度。備註
如果
GetStringAsync(因此getStringTask)在GetUrlContentLengthAsync等候之前完成,則控制權會保留在GetUrlContentLengthAsync中。 如果呼叫的非同步進程getStringTask已完成,而GetUrlContentLengthAsync不必等待最終結果,暫停然後返回的資源支出將被浪費。在呼叫方法中,處理模式會繼續。 呼叫端可能會在等待結果
GetUrlContentLengthAsync之前,執行其他不依賴於該結果的工作,或者呼叫端可能會立即等待該結果。 呼叫方法正在等候GetUrlContentLengthAsync,而且GetUrlContentLengthAsync正在等候GetStringAsync。GetStringAsync會完成併產生字串結果。 呼叫GetStringAsync不會以您預期的方式傳回字串結果。 (請記住,方法已在步驟 3 中傳回工作。相反地,字串結果會儲存在表示方法完成的工作中,getStringTask。 await 運算符會從getStringTask擷取結果。 指定語句會將擷取的結果指派給contents。當 有字串結果時
GetUrlContentLengthAsync,方法可以計算字串的長度。 接著,GetUrlContentLengthAsync的工作也會完成,而等候的事件處理程式可以繼續。 在文章結尾的完整範例中,您可以確認事件處理常式會擷取並列印長度結果的值。 如果您是非同步程式設計的新手,請花點時間考慮同步和非同步行為之間的差異。 同步方法會在工作完成時傳回 (步驟 5),但異步方法會在工作暫停時傳回工作值(步驟 3 和 6)。 當異步方法最終完成其工作時,工作會標示為已完成,如果有任何,則結果會儲存在工作中。
API 異步方法
您可能想知道在哪裡可以找到支援非同步程式設計的方法,例如 GetStringAsync。 .NET Framework 4.5 或更高版本和 .NET Core 包含許多使用 async 和 await的成員。 您可以透過附加至成員名稱的「非同步」尾碼,以及其傳回類型 Task 或 Task<TResult>來識別它們。 例如,類別System.IO.Stream包含方法CopyToAsync、ReadAsync 和 WriteAsync,以及同步方法CopyTo、Read 和 Write。
Windows 執行環境也包含許多方法,您可以在 Windows 應用程式中搭配 async 和 await 使用。 如需詳細資訊,請參閱 UWP 開發的 線程和異步程序 設計,以及 異步程式設計(Windows 市集應用程式) 和快速入門:如果您使用舊版 Windows 運行時間 ,請在 C# 或 Visual Basic 中呼叫異步 API 。
線程
非同步方法旨在進行不阻塞的操作。
await異步方法中的表達式不會在等候的工作執行時封鎖目前的線程。 相反地,表達式會將方法的其餘部分註冊為接續,並將控制權傳回至異步方法的呼叫端。
async
await 關鍵字不會導致建立額外的執行緒。 異步方法不需要多線程,因為異步方法不會在其自己的線程上執行。 方法會在目前的同步處理內容上執行,而且只有在方法處於使用中狀態時,才會在線程上使用時間。 您可以使用 Task.Run 將 CPU 密集型工作移至背景執行緒,但背景執行緒無法協助處理只是等待結果可用的程序。
幾乎在每種情況下,基於 async 的非同步程式設計方法都比現有的方法更可取。 特別是,這種方法比 I/O 限制的操作的BackgroundWorker類別更好,因為程式碼更簡單,而且您不需要防範競爭條件。 與 Task.Run 方法結合時,異步程式設計優於 BackgroundWorker CPU 系結作業,因為異步程式設計會將執行程式代碼的協調詳細數據與傳送至線程集區的工作 Task.Run 分開。
非同步並等待
如果您指定方法是異步方法,請使用 異步 修飾詞,您可以啟用下列兩項功能。
標示的異步方法可以使用 await 來指定暫停點。 運算符
await會告訴編譯程式,在等候的異步進程完成之前,異步方法無法繼續超過該點。 同時,控制權會傳回至異步方法的呼叫者。表達式中
await異步方法的暫停不會構成方法的結束,而且finally區塊不會執行。標記的異步方法本身可由呼叫它的方法等候。
異步方法通常包含一或多個運算子的 await 出現次數,但缺少 await 表達式並不會造成編譯程序錯誤。 如果異步方法不使用 await 運算符來標記暫停點,則該方法會像同步方法一樣執行,不論使用 async 修飾詞。 編譯程式會發出這類方法的警告。
async 和 await 是內容關鍵詞。 如需詳細資訊和範例,請參閱下列文章:
傳回類型和參數
異步方法通常會傳回Task或Task<TResult>。 在異步方法內, await 運算符會套用至從呼叫另一個異步方法傳回的工作。
如果方法包含一個指定 Task<TResult> 類型為運算數的 return 語句,您可以指定 TResult 作為傳回型別。
如果方法沒有 return 語句,或者包含不返回運算元的 return 語句,您可以使用 Task 作為傳回型別。
您也可以指定任何其他傳回類型 (如果類型包含 GetAwaiter 方法)。
ValueTask<TResult> 是這類類型的範例。 它可在 System.Threading.Tasks.Extension NuGet 套件中使用。
下列範例示範如何宣告及呼叫傳回 Task<TResult> 或 Task的方法:
async Task<int> GetTaskOfTResultAsync()
{
int hours = 0;
await Task.Delay(0);
return hours;
}
Task<int> returnedTaskTResult = GetTaskOfTResultAsync();
int intResult = await returnedTaskTResult;
// Single line
// int intResult = await GetTaskOfTResultAsync();
async Task GetTaskAsync()
{
await Task.Delay(0);
// No return statement needed
}
Task returnedTask = GetTaskAsync();
await returnedTask;
// Single line
await GetTaskAsync();
每個傳回的工作都代表進行中的工作。 任務包含有關異步進程狀態的信息,最後結果要么是進程的最終結果,要么是在進程不成功時出現的例外狀況。
異步方法也可以具有void的回傳型別。 此傳回型別主要用於需要void傳回型別的事件處理常式定義。 異步事件處理程式通常做為異步程式的起點。
無法等候具有 void 傳回型別的異步方法,且 void 傳回方法的呼叫端無法攔截方法擲回的任何例外狀況。
異步方法無法宣告 in、 ref 或 out 參數,但方法可以呼叫具有這類參數的方法。 同樣地,非同步方法無法以傳址方式傳回值,雖然它可以呼叫具有引用傳回值的方法。
如需詳細資訊和範例,請參閱 異步傳回型別 (C#) 。
在 Windows 執行階段程式設計中的非同步 API 具有以下其中一種傳回類型,這些類型類似於任務:
- IAsyncOperation<TResult>,對應至 Task<TResult>
- IAsyncAction,對應至 Task
- IAsyncActionWithProgress<TProgress>
- IAsyncOperationWithProgress<TResult,TProgress>
命名慣例
依照慣例,傳回常用可等候型別的方法(例如、Task、Task<T>ValueTask、ValueTask<T>) 應該有結尾為 “Async” 的名稱。 啟動非同步作業但未傳回可等候類型的方法不應該具有以 “Async” 結尾的名稱,但可能以 “Begin”、“Start” 或其他動詞開頭,以建議此方法不會傳回或擲回作業的結果。
您可以忽略事件、基類或介面合約建議不同名稱的慣例。 例如,您不應該重新命名常見的事件處理程式,例如 OnButtonClick。
相關文章 (Visual Studio)
| 標題 | 說明 |
|---|---|
| 如何使用 async 和 await 平行提出多個 Web 要求(C#) | 示範如何同時啟動數個工作。 |
| 異步傳回型別 (C#) | 說明異步方法可以傳回的類型,並說明每個類型何時適當。 |
| 以取消令牌作為訊號機制來取消工作。 | 示範如何將下列功能新增至您的異步解決方案: - 取消工作清單 (C#) - 在一段時間後取消工作 (C#) - 異步工作完成時即刻進行處理 (C#) |
| 使用異步進行檔案存取 (C#) | 列出並示範使用 async 和 await 來存取檔案的優點。 |
| 工作型非同步模式 (TAP) | 描述非同步模式。 模式是以Task和Task<TResult>類型為基礎。 |
| Channel 9 上的非同步影片 | 提供有關非同步編程的各種視頻的鏈接。 |