使用非同步編程可以避免性能瓶頸,提高應用程序的整體響應速度。然而,傳統的編寫非同步應用程序的技術可能會很複雜,使其難以編寫、調試和維護。
C# 支持簡化的方法,即非同步編程,它利用了 .NET 執行時(runtime)的非同步支持。編譯器完成了開發人員過去要做的艱巨工作,而你的應用程序則保留了類似同步代碼的邏輯結構。因此,你只需做一小部分工作就可以獲得非同步編程的所有好處。
本文概述了何時以及如何使用非同步編程,並包含指向詳細信息和示例的鏈接。
非同步提升響應能力#
非同步對於可能阻塞的活動非常重要,比如網絡訪問。訪問網絡資源有時會很慢或延遲。如果這樣的活動在同步過程中被阻塞,整個應用程序必須等待。而在非同步過程中,應用程序可以繼續進行其他不依賴於網絡資源的工作,直到可能阻塞的任務完成。
下表顯示了非同步編程改善響應性能力的典型領域。列出了來自 .NET 和 Windows Runtime 的 APIs 包含支持非同步編程的方法。
非同步對於訪問 UI 線程的應用程序尤為重要,因為所有與 UI 相關的活動通常共享一個線程。如果同步應用程序中的任何進程被阻塞,那麼所有進程都會被阻塞。您的應用程序停止響應,並且你可能會認為它已經失敗了,而實際上它只是在等待。
當你使用非同步方法時,應用程序將繼續響應 UI 操作。例如,你可以調整窗口大小或最小化窗口,或者如果不想等待它完成,則可以關閉該應用程序。
在設計非同步操作時,基於非同步的方法在選項列表中添加了相當於自動傳輸的選項。也就是說,開發人員只需要投入較少的工作量即可獲得傳統非同步編程的所有優點。
非同步方法易於編寫(Async methods are easy to write)#
使用 C# 中的 async 和 await 關鍵字是非同步編程的核心。通過使用這兩個關鍵字,您可以使用 .NET Framework、.NET Core 或 Windows Runtime 中的資源來創建非同步方法,就像創建同步方法一樣簡單。使用 async 關鍵字定義的非同步方法稱為 async 方法。
下面的示例展示了一個 async 方法。
你可以從 Asynchronous programming with async and await in C# 下載完整的 Windows Presentation Foundation(WPF)示例。
public async Task<int> GetUrlContentLengthAsync()
{
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>
(有關更多選項,請參閱 “Return Types” 部分)。方法名稱以 Async 結尾。在方法體中,GetStringAsync
返回一個 Task<string>
。這意味著當你等待任務完成時,會得到一個字符串(內容)。在等待 Task 之前,可以執行不依賴於 GetStringAsync
返回的字符串的工作。
請特別注意 await
操作符:
GetUrlContentLengthAsync
無法繼續執行直到getStringTask
完成。- 同時,控制權回歸
GetUrlContentLengthAsync
的調用者。 - 當
getStringTask
完成後,在此處恢復控制。 - 然後,
await
操作符從getStringTask
檢索字符串結果。
return 語句指定了一個整數結果。任何正在等待 GetUrlContentLengthAsync
的方法都會獲取長度值。
如果 GetUrlContentLengthAsync
在調用 GetStringAsync
和等待其完成之間沒有其他工作可做,則可以通過以下單個語句來簡化代碼:
string contents = await client.GetStringAsync("https://learn.microsoft.com/dotnet");
以下特點總結了前面示例中的非同步方法:
- 方法簽名包含
async
修飾符。 - 非同步方法的名稱通常以 "Async" 後綴結尾。
- 返回類型可以是以下類型之一:
-
如果方法中有返回語句且操作數的類型為
TResult
,則返回 Task。 -
如果方法沒有返回語句或者返回語句沒有操作數,則返回 Task。
-
如果編寫非同步事件處理程序,則返回
void
。 -
其他具有
GetAwaiter
方法的任何其他類型。更多信息,請參閱 Return types and parameters 部分。
-
- 該方法通常包含至少一個
await
表達式,它標記了一個點,在該點上,直到等待的非同步操作完成方法才能繼續。同時,該方法被掛起,並將控制權返還給調用方。本文的下一節將說明在掛起點會發生什麼。
在非同步方法中,使用提供的關鍵字和類型來指示要做什麼,而編譯器則負責其餘工作,包括跟蹤當控制權返回到掛起狀態下的 await 點時必須發生的事情。一些常規流程(例如循環和異常處理)在傳統非同步代碼中可能難以處理。但在非同步方法中,你可以像在同步解決方案一樣編寫這些元素,問題也就迎刃而解了。
若要詳細了解舊版 .NET Framework 中的非同步性,請參閱 TPL 和傳統 .NET Framework 非同步編程。
非同步方法的運行機制(What happens in an async method)#
非同步編程中最重要的是理解控制流(control flow)如何從一個方法移動到另一個方法。下圖將引導你完成這個過程:
當調用方調用非同步方法時,圖中的數字與以下步驟相對應:
-
調用方調用並等待
GetUrlContentLengthAsync
非同步方法。 -
GetUrlContentLengthAsync
創建一個 HttpClient 實例,並調用GetStringAsync
非同步方法以字符串形式下載網站內容。 -
在
GetStringAsync
中發生了一些掛起進度(progress)的事情。可能需要等待網站下載或其他阻塞活動。為避免阻塞資源,GetStringAsync
將控制權交給(yield control)其調用者GetUrlContentLengthAsync
。
GetStringAsync
返回一個 Task,其中TResult
是一個字符串,並且GetUrlContentLengthAsync
將任務分配給getStringTask
變量。該任務表示對GetStringAsync
的調用正在進行中,承諾在工作完成時產生實際的字符串值。 -
由於尚未 await
getStringTask
,GetUrlContentLengthAsync
可以繼續進行其他工作,而不依賴於GetStringAsync
的最終結果。這些工作(繼續進行其他工作)通過調用同步方法DoIndependentWork
來表示。 -
DoIndependentWork
是一個同步方法,它完成工作後返回給調用者。 -
GetUrlContentLengthAsync
已經沒有在getStringTask
結果出來前需要完成的工作了。接下來,GetUrlContentLengthAsync
希望計算並返回已下載字符串的長度,但是在獲取到該字符串之前無法計算該值。
因此,GetUrlContentLengthAsync
使用 await 運算符掛起進度,並將控制權交給調用GetUrlContentLengthAsync
的方法。GetUrlContentLengthAsync
向調用者返回一個Task<int>
。該任務表示承諾產生一個整數結果,即下載字符串的長度。在調用方內部,處理模式仍在繼續。在等待
GetUrlContentLengthAsync
的結果之前,調用者可能會執行不依賴於GetUrlContentLengthAsync
結果的其他工作,或者調用者可能會立即等待。調用方正在等待GetUrlContentLengthAsync
,而GetUrlContentLengthAsync
正在等待GetStringAsync
。 -
GetStringAsync
完成並生成一個字符串結果。所返回的字符串結果並不是你所期望的那樣。(請記住,在第 3 步中,該方法已經返回了一個 Task)。相反,字符串結果存儲在代表方法完成的任務getStringTask
中。await 運算符從getStringTask
取回結果。賦值語句將取回到的結果賦值給contents
變量。 -
當
GetUrlContentLengthAsync
獲得字符串結果時,該方法可以計算出字符串長度。然後,GetUrlContentLengthAsync
也完成了工作,並且等待事件處理程序可以恢復執行。
在本文末尾提供完整示例中,可以確認事件處理程序取回和打印長度的值。如果你剛開始接觸非同步編程,請花一分鐘時間考慮同步和非同步行為之間的區別。同步方法在其工作完成時返回(第 5 步),但非同步方法在其工作暫停時返回任務值(第 3 和 6 步)。當非同步方法最終完成其工作時,任務被標記為已完成,而結果(如果有)將存儲在任務中。
API 非同步方法(API async methods)#
.NET Framework 4.5+ 和 .NET Core 包含許多支持非同步編程的方法,可以通過成員名稱後綴 "Async" 以及返回類型為 Task 或 Task 來識別它們。例如,System.IO.Stream
類中有CopyToAsync、ReadAsync 和 WriteAsync 等方法,以及同步方法 CopyTo、Read 和 Write。
Windows Runtime 也包含許多可在 Windows 應用程序中使用 async
和 await
的方法。有關更多信息,請參閱 UWP 開發的線程和非同步編程以及 Asynchronous programming (Windows Store apps) 和 Quickstart: Calling asynchronous APIs in C# or Visual Basic(如果你使用較早版本的 Windows Runtime)。
線程(Thread)#
非同步方法旨在實現非阻塞(non-blocking)操作。當等待的任務正在運行時,**await
表達式不會阻塞當前線程 **。相反,表達式會將方法的其餘部分標記為繼續,並將控制權返回給 async 方法的調用者。
**async
和 await
關鍵字不會創建額外的線程。非同步方法不需要多線程,因為它不在自己的線程上運行 **。該方法在當前同步上下文(context)中運行,並且僅當該方法處於活動狀態時才使用線程上的時間。可以使用 Task.Run 將 CPU-bound 工作移至後台線程,但後台線程對於等待結果可用性的過程沒有幫助。
與現有方案相比,在幾乎所有情況下,基於 async 的非同步編程方式更可取。特別是對於 I/O-bound 操作來說,非同步方法優於 BackgroundWorker 類,因為代碼更簡單且無需防止競爭條件發生。結合 Task.Run 方法使用時,非同步編程也優於 BackgroundWorker 類進行 CPU-bound 操作,因為非同步編程將運行代碼的協調細節與 Task.Run
轉移到線程池的工作分離開來。
async 和 await#
如果使用 async 修飾符指定一個方法為非同步方法,就會啟用以下兩種功能:
-
被標記的 async 方法可以使用 await 來指定掛起點。
await
操作符會告訴編譯器,在等待的非同步過程完成之前,非同步方法不能在該掛起點後繼續執行。在此期間,控制權返回給非同步方法的調用者。在
await
表達式處掛起的非同步方法並不會造成方法退出,而且 finally 塊也不會運行。 -
被標記的 async 方法本身,可以被調用它的方法等待。
一個 async 方法通常包含一個或多個 await
操作符,但如果沒有 await
表達式,編譯器也不會報錯(只是發出警告)。如果一個 async 方法沒有使用 await
操作符來標記掛起點,那麼儘管有 async
修飾符,該方法還是會像同步方法一樣執行。編譯器會對此類方法發出警告,強烈建議不要這樣做,因為這樣的代碼非常低效。
async
和 await
是上下文關鍵字。有關詳細信息和示例,請參閱以下主題:
返回類型和參數(Return types and parameters)#
在非同步方法中,通常返回一個 Task 或 Task。在非同步方法內部,await
操作符用作從另一個非同步方法調用後返回的 Task。
如果該方法包含指定 TResult
類型的操作數的 return 語句,則將 Task 指定為返回類型。
如果該方法沒有 return 語句或者包含不返回操作數的 return 語句,則使用 Task 作為返回類型。
還可以指定任何其他返回類型,只要該類型包含 GetAwaiter
方法即可。ValueTask 就是這種類型,在 System.Threading.Tasks.Extension NuGet 軟件包中提供。
以下示例顯示了如何聲明和調用返回 Task 或者 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();
每個返回的 Task
代表著正在進行的工作。Task
封裝了非同步過程狀態的信息,最終可能是來自該過程的最終結果或者該過程引發的異常(如果不成功)。
非同步方法也可以有 void
返回類型。這種返回類型主要用於定義事件處理程序(event handler),其需要 void 返回類型。非同步事件處理程序通常作為非同步程序的起點。
具有 void
返回類型的非同步方法不能被等待,並且調用者無法捕獲該方法拋出的任何異常。
非同步方法不能聲明 in、ref 或 out 參數,但可以調用具有此類參數的方法。同樣地,非同步方法不能通過引用返回值,但可以調用具有 ref 返回值的方法。
更多信息和示例,請參閱 Async return types (C#)。
Windows runtime 編程中的非同步 API 具有下列返回類型之一(類似於 Task):
- IAsyncOperation, 對應 Task
- IAsyncAction, 對應 Task
- IAsyncActionWithProgress
- IAsyncOperationWithProgress<TResult,TProgress>
命名約定(Naming convention)#
按照慣例,返回常見可等待類型的方法(例如 Task
、Task<T>
、ValueTask
、ValueTask<T>
)應該以 "Async" 結尾。開始非同步操作但不返回可等待類型的方法不應該以 "Async" 結尾命名,但可以以 "Begin"、"Start" 或其他動詞開頭來暗示此方法不會返回或拋出操作結果。
如果事件(event)、基類(base class)或接口契約(interface contract)建議使用不同的名稱,則可以忽略該約定(以 “Async” 結尾)。例如,您不應重命名常見的事件處理程序,如 OnButtonClick
。