非同步工作程式設計模型

您可以使用非同步程式設計,避免發生效能瓶頸並增強應用程式的整體回應性。 不過,撰寫非同步應用程式的傳統技術可能很複雜,因而難以撰寫、偵錯和維護。

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# 中的 asyncawait 關鍵字都是非同步程式設計的核心。 您可以利用那兩個關鍵字,使用 .NET Framework、.NET Core 或 Windows 執行階段中的資源來建立非同步方法,幾乎就像建立同步方法一樣容易。 使用 async 關鍵字的非同步方法就稱為非同步方法

下列範例將示範非同步方法。 程式碼中幾乎所有內容對您而言應該都很熟悉。

您可以從在 C# 中使用 async 和 await 進行非同步程式設計中找到完整的 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> (請參閱<傳回型別>一節以取得更多選項)。 方法名稱結尾為 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 方法的其他型別。

    如需詳細資訊,請參閱傳回型別和參數一節。

  • 方法通常至少包含一個 await 運算式,表示方法在等候的非同步作業完成後才能繼續的點。 此時,方法已暫停,而且控制權返回到方法的呼叫端。 本主題的下一節將說明暫停點會發生什麼情況。

在非同步方法中,您會使用提供的關鍵字和類型表示您想要執行的工作,而編譯器會完成其餘的部分,包括追蹤控制權返回已暫停方法中的等候點時必須進行的作業。 某些常式處理序像是迴圈和例外狀況處理,在傳統非同步程式碼中可能不容易處理。 在非同步方法中,您可以像在同步方案中一樣撰寫這些項目,如此就可以解決這個問題了。

如需舊版 .NET Framework 中非同步功能的詳細資訊,請參閱 TPL 和傳統 .NET Framework 非同步程式設計

非同步方法中執行了哪些工作

在非同步程式設計中要了解的最重要事情,就是控制流程如何在方法之間移動。 下圖將引導您了解整個程序:

Trace navigation of async control flow

圖表中的數字會對應至下列步驟,在呼叫方法呼叫非同步方法時起始。

  1. 呼叫方法會呼叫並等候 GetUrlContentLengthAsync 非同步方法。

  2. GetUrlContentLengthAsync 會建立 HttpClient 執行個體並呼叫 GetStringAsync 非同步方法,將網站的內容當做字串下載。

  3. GetStringAsync 中發生了導致進度暫停的一些狀況。 可能必須等待網站下載或其他封鎖活動。 為了避免封鎖資源,GetStringAsync 會將控制權遞交 (Yield) 給它的呼叫端 GetUrlContentLengthAsync

    GetStringAsync 會傳回 Task<TResult> (其中 TResult 是字串),而 GetUrlContentLengthAsync 則會將工作指派給 getStringTask 變數。 工作代表對 GetStringAsync 之呼叫的進行中程序,並承諾會在工作完成時產生實際字串值。

  4. 因為尚未等候 getStringTask,所以 GetUrlContentLengthAsync 可以繼續進行其他不相依於 GetStringAsync 之最終結果的其他工作。 這項工作是由對同步方法 DoIndependentWork 的呼叫來表示。

  5. DoIndependentWork 是完成其工作並傳回其呼叫端的同步方法。

  6. GetUrlContentLengthAsync 已完成所有可處理的工作,但未取得來自 getStringTask 的結果。 GetUrlContentLengthAsync 接著要計算和傳回下載字串的長度,但是方法必須等到有字串時才能計算該值。

    因此,GetUrlContentLengthAsync 會使用 await 運算子暫停其進度,並將控制權遞交 (Yield) 給呼叫 GetUrlContentLengthAsync 的方法。 GetUrlContentLengthAsync 會將 Task<int> 傳回呼叫端。 這項工作代表承諾會產生相當於下載字串長度的整數結果。

    注意

    如果 GetStringAsync (和 getStringTask) 在 GetUrlContentLengthAsync 等候它之前先完成,控制權仍會留在 GetUrlContentLengthAsync。 如果呼叫的非同步處理序 getStringTask 已經完成,且 GetUrlContentLengthAsync 無需等候最終結果,那麼暫止然後返回 GetUrlContentLengthAsync 的支出就是一種浪費。

    在呼叫方法內,處理模式會繼續。 呼叫端可能會在等候結果之前執行其他不取決於 GetUrlContentLengthAsync 之結果的工作,或者呼叫端可能立即等候。 呼叫方法正在等候 GetUrlContentLengthAsync,且 GetUrlContentLengthAsync 正在等候 GetStringAsync

  7. GetStringAsync 完成並產生字串結果。 字串結果不會依照您預期的方式透過呼叫 GetStringAsync 來傳回 (請記住,方法已在步驟 3 傳回工作)。字串結果會改為儲存在表示方法 getStringTask 完成的工作中。 await 運算子會從 getStringTask 擷取結果。 指派陳述式會將擷取的結果指派給 contents

  8. GetUrlContentLengthAsync 擁有字串結果時,方法就可以計算字串的長度。 然後 GetUrlContentLengthAsync 的工作也已完成,而且等候事件處理常式可以繼續執行。 在本主題最後的完整範例中,您可以確認事件處理常式會擷取並列印長度結果的值。 如果您不熟悉非同步程式設計,請花一分鐘思考同步和非同步行為之間的差異。 同步方法會在其工作完成時傳回 (步驟 5),而非同步方法則會在其工作暫停時傳回工作值 (步驟 3 和步驟 6)。 當非同步方法最後完成其工作時,工作會標示為已完成,而結果 (如果有的話) 會儲存在工作中。

API 非同步方法

您可能會想知道哪裡可以找到支援非同步程式設計的方法,例如 GetStringAsync。 .NET Framework 4.5 或更新版本及 .NET Core 包含許多使用 asyncawait 的成員。 您也可以依附加至成員名稱的 "Async" 尾碼,以及依其傳回型別 TaskTask<TResult> 來進行辨識。 例如,相對於同步方法 CopyToReadWriteSystem.IO.Stream 類別也包含一些方法,例如 CopyToAsyncReadAsyncWriteAsync

Windows 執行階段也包含許多您可以在 Windows 應用程式中與 asyncawait 搭配使用的方法。 如需詳細資訊,請參閱適用於 UWP 開發的執行緒和非同步程式設計,如果您使用舊版的 Windows 執行階段,則請參閱非同步程式設計 (Microsoft Store 應用程式)快速入門:在 C# 或 Visual Basic 中呼叫非同步 API

執行緒

非同步方法主要做為非封鎖作業使用。 當等候的工作正在執行時,非同步方法的 await 運算式不會封鎖目前的執行緒。 運算式會改為註冊方法的其餘部分做為接續,並將控制權交還給非同步方法的呼叫端。

asyncawait 關鍵字不會導致建立其他執行緒。 由於非同步方法不會在本身的執行緒上執行,因此非同步方法不需要多執行緒。 方法會在目前的同步處理內容執行,而且只有在方法為作用中時才會在執行緒上花費時間。 您可以使用 Task.Run 將受限於 CPU 的工作移到背景執行緒,但是背景執行緒無法協助處理正在等待結果產生的處理序。

非同步程式設計的非同步方法幾乎是所有案例的現有方法當中較好的方法。 特別是,這個方法比受限於 I/O 作業的 BackgroundWorker 類別還要好,因為程式碼較簡單,而且不需要防範競爭條件。 與 Task.Run 方法合併時,非同步程式設計會比 BackgroundWorker 更適合用來進行 CPU 繫結作業,因為非同步程式設計會將執行程式碼的協調細節,與 Task.Run 傳輸至執行緒集區的工作分隔開來。

async 與 await

如果您使用 async 修飾詞來將方法指定為非同步方法,就會啟用下列兩項功能。

  • 標記的非同步方法可以使用 await 來指定暫停點。 await 運算子會告知編譯器,非同步方法只有在等候的非同步處理序完成後,才能繼續通過該點。 同時,控制權會返回非同步方法的呼叫端。

    非同步方法在 await 運算式上暫停時,並不構成從方法中退出,而 finally 區塊也不會執行。

  • 標記的非同步方法本身可以做為其呼叫方法的等候目標。

非同步方法中通常會出現一或多次 await 運算子,但是沒有 await 運算式也不會造成編譯器錯誤。 如果非同步方法未使用 await 運算子來標記暫停點,則即使有 async 修飾元,方法仍會像同步方法一樣執行。 編譯器將對這類方法發出警告。

asyncawait 都是內容關鍵字。 如需詳細資訊和範例,請參閱下列主題:

傳回型別和參數

async 方法通常會傳回 TaskTask<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 的方法呼叫者無法攔截該方法擲回的任何例外狀況。

非同步方法無法宣告 inrefout 參數,但是此方法可以呼叫具有這類參數的方法。 同樣地,async 方法無法以傳址方式傳回值,但可以使用 ref 傳回值呼叫方法。

如需詳細資訊和範例,請參閱非同步傳回型別 (C#)

Windows 執行階段程式設計中的非同步 API 具有下列其中一種傳回型別 (類似於工作):

命名慣例

依照慣例,若方法會傳回通常可等候的型別 (例如,TaskTask<T>ValueTaskValueTask<T>),則其名稱結尾應為 "Async"。 會開始執行非同步作業但不會傳回可等候類型的方法不應該具有結尾為 "Async" 的名稱,但其名稱開頭可以是 "Begin"、"Start" 或一些其他動詞,以建議此方法不會傳回或擲回作業結果。

當事件、基底類別或介面合約採用不同的名稱時,您可以忽略慣例。 例如,您不應該重新命名通用事件處理常式 (像是 OnButtonClick)。

相關文章 (Visual Studio)

標題 描述
如何使用 async 與 await 同時發出多個 Web 要求 (C#) 示範如何同時啟動數個工作。
非同步傳回型別 (C#) 說明非同步方法可以傳回的型別,並解釋每種型別的適用時機。
使用取消權杖作為訊號機制來取消工作。 顯示如何將下列功能加入至您的非同步方案:

- 取消工作清單 (C#)
- 在一段時間後取消工作 (C#)
- 在非同步工作完成時進行處理 (C#)
使用非同步功能存取檔案 (C#) 列出並示範使用 async 和 await 存取檔案的優點。
以工作為基礎的非同步模式 (TAP) 描述非同步模式,此模式是以 TaskTask<TResult> 型別為基礎。
Channel 9 上的非同步影片 提供有關非同步程式設計的各種不同視訊連結。

另請參閱