非同步工作程式設計模型
您可以使用非同步程式設計,避免發生效能瓶頸並增強應用程式的整體回應性。 不過,撰寫非同步應用程式的傳統技術可能很複雜,因而難以撰寫、偵錯和維護。
C# 支援簡化的方法 (非同步程式設計),其充分運用了 .NET 執行階段中的非同步支援。 編譯器會代替開發人員處理過去經常要處理的困難工作,而您的應用程式仍保有類似同步程式碼的邏輯結構。 因此,您可以輕鬆擁有非同步程式設計的所有優點。
本主題提供使用非同步程式設計的時機和使用方式的概觀,並加入包含詳細資料及範例的支援主題連結。
非同步可改善回應性
非同步對於可能在像是 Web 存取時會進行封鎖的活動而言相當重要。 存取 Web 資源的速度有時會變慢或延遲。 如果這類活動在同步處理序中遭到封鎖,整個應用程式就必須等候。 在非同步處理序中,應用程式可以繼續處理其他與 Web 資源不相關的工作,直到可能的封鎖工作完成。
下表顯示非同步程式設計一般會改善回應速度的部分。 從 .NET 和 Windows 執行階段列出的 API 包含支援非同步程式設計的方法。
應用程式區域 | 使用非同步方法的 .NET 類型 | 使用非同步方法的 Windows 執行階段類型 |
---|---|---|
Web 存取 | HttpClient | Windows.Web.Http.HttpClient SyndicationClient |
處理檔案 | JsonSerializer StreamReader StreamWriter XmlReader XmlWriter |
StorageFile |
使用映像 | MediaCapture BitmapEncoder BitmapDecoder |
|
WCF 程式設計 | 同步和非同步作業 |
非同步對於存取 UI 執行緒的應用程式而言確實特別有用,因為所有 UI 相關活動通常都會共用一個執行緒。 如果同步應用程式中有任何處理序遭到封鎖,所有處理序都會遭到封鎖。 您的應用程式會停止回應,而且您可能會認為應用程式失敗,但實際上只是在等候。
當您使用非同步方法時,應用程式會繼續回應 UI。 例如,您可以調整視窗大小或將視窗縮到最小,如果不想要等待應用程式完成,也可以將它關閉。
非同步方法會在設計非同步作業時,於選項清單中加入自動傳輸的對等項目讓您選擇。 也就是說,除了擁有傳統非同步程式設計的所有優點之外,開發人員所需投入的時間也大為減少。
非同步方法比較容易撰寫
C# 中的 async 和 await 關鍵字都是非同步程式設計的核心。 您可以利用那兩個關鍵字,使用 .NET Framework、.NET Core 或 Windows 執行階段中的資源來建立非同步方法,幾乎就像建立同步方法一樣容易。 使用 async
關鍵字的非同步方法就稱為非同步方法。
下列範例將示範非同步方法。 程式碼中幾乎所有內容對您而言應該都很熟悉。
您可以從在 C# 中使用 async 和 await 進行非同步程式設計中找到完整的 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
)。 等候工作之前,您可以從 GetStringAsync
執行不依賴 string
的工作。
密切注意 await
運算子。 其會暫止 GetUrlContentLengthAsync
:
GetUrlContentLengthAsync
無法繼續,直到getStringTask
完成為止。- 同時,控制項會傳回
GetUrlContentLengthAsync
的呼叫端。 - 當
getStringTask
完成時,控制項會繼續這裡執行。 - 接著,
await
運算子會從getStringTask
擷取string
結果。
return 陳述式會指定整數結果。 正在等待 GetUrlContentLengthAsync
的任何方法都會擷取長度值。
如果 GetUrlContentLengthAsync
在呼叫 GetStringAsync
與等候其完成之間沒有任何可以執行的工作,您可以在下列單一陳述式中呼叫和等候,以簡化程式碼。
string contents = await client.GetStringAsync("https://learn.microsoft.com/dotnet");
下列特性摘要說明上述範例為非同步方法的原因:
方法簽章包含
async
修飾詞。按照慣例,非同步方法的名稱是以 "Async" 後置字元為結尾。
傳回型別是下列其中一種類型:
- 如果方法的 return 陳述式中運算元的類型為
TResult
,則為 Task<TResult>。 - 如果方法沒有 return 陳述式或是 return 陳述式沒有運算元,則為 Task。
- 如果您撰寫的是非同步事件處理常式,則為
void
。 - 任何具有
GetAwaiter
方法的其他型別。
如需詳細資訊,請參閱傳回型別和參數一節。
- 如果方法的 return 陳述式中運算元的類型為
方法通常至少包含一個
await
運算式,表示方法在等候的非同步作業完成後才能繼續的點。 此時,方法已暫停,而且控制權返回到方法的呼叫端。 本主題的下一節將說明暫停點會發生什麼情況。
在非同步方法中,您會使用提供的關鍵字和類型表示您想要執行的工作,而編譯器會完成其餘的部分,包括追蹤控制權返回已暫停方法中的等候點時必須進行的作業。 某些常式處理序像是迴圈和例外狀況處理,在傳統非同步程式碼中可能不容易處理。 在非同步方法中,您可以像在同步方案中一樣撰寫這些項目,如此就可以解決這個問題了。
如需舊版 .NET Framework 中非同步功能的詳細資訊,請參閱 TPL 和傳統 .NET Framework 非同步程式設計。
非同步方法中執行了哪些工作
在非同步程式設計中要了解的最重要事情,就是控制流程如何在方法之間移動。 下圖將引導您了解整個程序:
圖表中的數字會對應至下列步驟,在呼叫方法呼叫非同步方法時起始。
呼叫方法會呼叫並等候
GetUrlContentLengthAsync
非同步方法。GetUrlContentLengthAsync
會建立 HttpClient 執行個體並呼叫 GetStringAsync 非同步方法,將網站的內容當做字串下載。GetStringAsync
中發生了導致進度暫停的一些狀況。 可能必須等待網站下載或其他封鎖活動。 為了避免封鎖資源,GetStringAsync
會將控制權遞交 (Yield) 給它的呼叫端GetUrlContentLengthAsync
。GetStringAsync
會傳回 Task<TResult> (其中TResult
是字串),而GetUrlContentLengthAsync
則會將工作指派給getStringTask
變數。 工作代表對GetStringAsync
之呼叫的進行中程序,並承諾會在工作完成時產生實際字串值。因為尚未等候
getStringTask
,所以GetUrlContentLengthAsync
可以繼續進行其他不相依於GetStringAsync
之最終結果的其他工作。 這項工作是由對同步方法DoIndependentWork
的呼叫來表示。DoIndependentWork
是完成其工作並傳回其呼叫端的同步方法。GetUrlContentLengthAsync
已完成所有可處理的工作,但未取得來自getStringTask
的結果。GetUrlContentLengthAsync
接著要計算和傳回下載字串的長度,但是方法必須等到有字串時才能計算該值。因此,
GetUrlContentLengthAsync
會使用 await 運算子暫停其進度,並將控制權遞交 (Yield) 給呼叫GetUrlContentLengthAsync
的方法。GetUrlContentLengthAsync
會將Task<int>
傳回呼叫端。 這項工作代表承諾會產生相當於下載字串長度的整數結果。注意
如果
GetStringAsync
(和getStringTask
) 在GetUrlContentLengthAsync
等候它之前先完成,控制權仍會留在GetUrlContentLengthAsync
。 如果呼叫的非同步處理序getStringTask
已經完成,且GetUrlContentLengthAsync
無需等候最終結果,那麼暫止然後返回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
的成員。 您也可以依附加至成員名稱的 "Async" 尾碼,以及依其傳回型別 Task 或 Task<TResult> 來進行辨識。 例如,相對於同步方法 CopyTo、Read 和 Write,System.IO.Stream
類別也包含一些方法,例如 CopyToAsync、ReadAsync 和 WriteAsync。
Windows 執行階段也包含許多您可以在 Windows 應用程式中與 async
和 await
搭配使用的方法。 如需詳細資訊,請參閱適用於 UWP 開發的執行緒和非同步程式設計,如果您使用舊版的 Windows 執行階段,則請參閱非同步程式設計 (Microsoft Store 應用程式) 和快速入門:在 C# 或 Visual Basic 中呼叫非同步 API。
執行緒
非同步方法主要做為非封鎖作業使用。 當等候的工作正在執行時,非同步方法的 await
運算式不會封鎖目前的執行緒。 運算式會改為註冊方法的其餘部分做為接續,並將控制權交還給非同步方法的呼叫端。
async
和 await
關鍵字不會導致建立其他執行緒。 由於非同步方法不會在本身的執行緒上執行,因此非同步方法不需要多執行緒。 方法會在目前的同步處理內容執行,而且只有在方法為作用中時才會在執行緒上花費時間。 您可以使用 Task.Run 將受限於 CPU 的工作移到背景執行緒,但是背景執行緒無法協助處理正在等待結果產生的處理序。
非同步程式設計的非同步方法幾乎是所有案例的現有方法當中較好的方法。 特別是,這個方法比受限於 I/O 作業的 BackgroundWorker 類別還要好,因為程式碼較簡單,而且不需要防範競爭條件。 與 Task.Run 方法合併時,非同步程式設計會比 BackgroundWorker 更適合用來進行 CPU 繫結作業,因為非同步程式設計會將執行程式碼的協調細節,與 Task.Run
傳輸至執行緒集區的工作分隔開來。
async 與 await
如果您使用 async 修飾詞來將方法指定為非同步方法,就會啟用下列兩項功能。
標記的非同步方法可以使用 await 來指定暫停點。
await
運算子會告知編譯器,非同步方法只有在等候的非同步處理序完成後,才能繼續通過該點。 同時,控制權會返回非同步方法的呼叫端。非同步方法在
await
運算式上暫停時,並不構成從方法中退出,而finally
區塊也不會執行。標記的非同步方法本身可以做為其呼叫方法的等候目標。
非同步方法中通常會出現一或多次 await
運算子,但是沒有 await
運算式也不會造成編譯器錯誤。 如果非同步方法未使用 await
運算子來標記暫停點,則即使有 async
修飾元,方法仍會像同步方法一樣執行。 編譯器將對這類方法發出警告。
async
和 await
都是內容關鍵字。 如需詳細資訊和範例,請參閱下列主題:
傳回型別和參數
async 方法通常會傳回 Task 或 Task<TResult>。 在非同步方法內,會將 await
運算子套用到呼叫另一個非同步方法所傳回的工作。
如果方法包含指定 TResult
型別運算元的 return
陳述式,請指定 Task<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 參數,但是此方法可以呼叫具有這類參數的方法。 同樣地,async 方法無法以傳址方式傳回值,但可以使用 ref 傳回值呼叫方法。
如需詳細資訊和範例,請參閱非同步傳回型別 (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 上的非同步影片 | 提供有關非同步程式設計的各種不同視訊連結。 |