在 .NET 中,基於任務的非同步模式是新開發推薦的非同步設計模式。 它是基於命名空間中的 Task 和 Task<TResult>System.Threading.Tasks 類型,代表非同步操作。
命名、參數和傳回類型
TAP 使用單一方法表示非同步作業的啟始和完成。 此方法與非同步程式設計模型(APM 或 IAsyncResult)模式及事件基礎非同步模式(EAP)形成對比。 APM 需要 Begin 和 End 方法。 EAP 需要具有 Async 尾碼的方法,也需要一或多個事件、事件處理常式委派類型和 EventArg衍生類型。 TAP 中的非同步方法會在傳回可等待類型的方法的作業名稱後面包含 Async 尾碼,例如 Task、 Task<TResult>、 ValueTask和 ValueTask<TResult>。 例如,非同步Get作業,傳回Task<String>的內容,可以命名為GetAsync。 如果您要將 TAP 方法新增至已包含具有尾碼的 Async EAP 方法名稱的類別,請改用尾碼 TaskAsync 。 例如,如果類別已經有一個 GetAsync 方法,請使用名稱 GetTaskAsync。 如果方法啟動非同步作業,但未傳回可等待的類型,則其名稱應該以 Begin, Start或其他動詞開頭,以建議此方法不會傳回或擲回作業的結果。
TAP 方法會根據對應的同步方法是否回傳 void 或類型 System.Threading.Tasks.Task, 回傳 System.Threading.Tasks.Task<TResult> 或 TResult。
TAP 方法的參數應該符合其同步對應方法的參數,而且應該以相同的順序提供。 但是, out 參數 ref 不受此規則的約束,應完全避免。 任何 out 或 ref 參數返回的數據都應該成為 Task<TResult> 返回的 TResult 的一部分,並應使用元組或自訂數據結構來容納多個值。 此外,即使 TAP 方法的同步對應專案未提供參數,也請考慮新增 CancellationToken 參數。
專門用於建立、操作或組合任務的方法(且方法的非同步意圖在方法名稱或所屬類型名稱中明確表示)不必遵循此命名模式。 這類方法通常被稱為 組合子。 組合器的範例包括 WhenAll 和 WhenAny,並在使用以任務為基礎的非同步模式一文的使用內建任務型組合器一節中討論。
如需 TAP 語法與舊版非同步程式設計模式 (例如非同步程式設計模型 (APM) 和事件型非同步模式 (EAP) ) 中使用的語法有何不同的範例,請參閱 非同步程式設計模式。
非同步行為、回傳類型與命名
關鍵字 async 不會強制方法在其他執行緒上非同步執行。 它啟用await,並且該方法會同步執行,直到遇到未完成的可等待項。 如果方法沒有遇到一個未完成的可等待對象,那麼它可以同步完成。
對大多數 API 來說,偏好以下回傳類型:
- 使用 Task 處理不產生值的非同步操作。
- 使用 Task<TResult> 來進行產生值的非同步操作。
- 僅在測量顯示分配壓力且消費者能承受額外使用限制時,才使用ValueTask或ValueTask<TResult>。
保持TAP命名可預測性:
- 請為回傳可等待型別的方法使用
Async後綴。 - 不要附加
Async到同步方法上。 - 將新的
MethodNameAsync多載與現有的 `MethodName` 一起加入。 不要移除或重新命名同步 API。 同時保留兩者,讓使用者能以自己的步調遷移,且不會造成中斷。
起始非同步作業
以 TAP 為基礎的非同步方法可以在傳回產生的工作之前,同步執行少量工作,例如驗證引數和起始非同步作業。 盡量減少同步工作,讓非同步方法能快速回傳。 快速退貨的原因包括:
- 你可能會從使用者介面(UI)執行緒中調用非同步方法,任何長時間執行的同步工作都可能損害應用程式的響應速度。
- 你可以同時啟動多個非同步方法。 因此,非同步方法同步部分中的任何長時間執行工作都可能延遲其他非同步作業的起始,從而減少並行的好處。
在某些情況下,完成作業所需的工作量小於非同步啟動作業所需的工作量。 在從串流進行讀取操作時,若讀取需求可以由已經在記憶體中緩衝的資料來滿足,這就是此類情境的例子。 在這種情況下,操作可能會同步完成,並可能回傳已完成的任務。
Exceptions
非同步方法應僅在使用錯誤時,直接從非同步方法呼叫拋出例外。 生產程式碼中不應發生使用錯誤。 例如,如果傳遞 Null 參考 (Nothing 在 Visual Basic 中) 作為方法的其中一個引數會導致錯誤狀態 (通常以例外狀況表示 ArgumentNullException ),您可以修改呼叫程式碼,以確保永遠不會傳遞 Null 參考。 對於其他錯誤,將非同步方法執行時發生的例外指派給回傳任務,即使非同步方法在任務回傳前同步完成。 一般而言,工作最多包含一個例外狀況。 然而,若任務代表多個操作(例如, WhenAll),則可能對同一任務有多個例外。
目標環境
當您實作 TAP 方法時,您可以判斷非同步執行發生的位置。 你可以選擇在執行緒池執行工作負載,或使用非同步 I/O(不綁定執行緒大部分時間),或在特定執行緒(例如 UI 執行緒)執行,或使用多種可能的上下文。 TAP 方法甚至可能沒有任何操作需要執行,可能只回傳 Task,表示系統其他部分發生的狀況(例如,任務代表資料抵達排隊資料結構)。
TAP 方法的呼叫者可以透過同步等待產生的任務來阻擋等待 TAP 方法完成,或在非同步操作完成時執行額外的(延續)程式碼。 接續程式碼的建立者可以控制該程式碼的執行位置。 你可以明確地建立續寫程式碼,例如透過 Task 類別的方法(例如 ContinueWith),或是隱式地使用建立在延續運算上的語言支援(例如 C# 的 await、Visual Basic 中的 Await、F# 中的 AwaitValue)。
任務狀態
類別 Task 提供非同步作業的生命週期,該週期由列舉 TaskStatus 表示。 為了支援由 Task 和 Task<TResult> 衍生的類型的極端情況,以及支援建構與排程的分離,類別 Task 提供了 Start 方法。 由公用 Task 建構函式建立的任務稱為 冷任務,因為它們的生命週期在非排程 Created 狀態下開始,只有當在這些實例上呼叫 Start 時才會進入排程狀態。
所有其他任務的生命週期開始時都是熱狀態,這表示它們所代表的非同步操作已經啟動,且其任務狀態為非 TaskStatus.Created列舉值。 必須啟動從 TAP 方法傳回的所有作業。 如果 TAP 方法在內部使用工作的建構函式來具現化要傳回的工作,則 TAP 方法必須於 Start 物件上呼叫 Task,才能傳回它。 使用 TAP 方法的使用者可以放心假設回傳的任務仍在進行中,且不應嘗試呼叫StartTask任何從 TAP 方法回傳的任務。 呼叫作用中工作Start,會導致InvalidOperationException例外狀況。
有關任務啟動後的「fire-and-forget」生命週期與擁有權問題,請參見保持非同步方法運行活躍。
取消(可選)
在 TAP 中,非同步方法實作者和非同步方法取用者都可以選擇取消。 如果作業允許取消,它會公開一個接受取消權杖(CancellationToken 實例)的非同步方法的多載。 依慣例,參數會命名 cancellationToken為 。
public static Task ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
Public Function ReadAsync(buffer As Byte(), offset As Integer, count As Integer,
cancellationToken As CancellationToken) As Task
非同步作業會監控此權杖以處理取消請求。 如果收到取消請求,可能會選擇接受該請求並取消該操作。 如果取消請求導致工作提前結束,TAP 方法會回傳一個任務,該任務結束於該 Canceled 狀態;沒有可用的結果,也不會拋出例外。
Canceled狀態與 和 FaultedRanToCompletion 狀態一起被視為任務的最終(已完成)狀態。 因此,如果任務處於狀態 Canceled ,則其 IsCompleted 屬性會傳回 true。 當工作在 Canceled 狀態完成時,任何與工作註冊的接續都會被排程或執行,除非指定了不接續的選項,例如 NotOnCanceled。 任何透過語言功能以非同步方式等候已取消任務的程式碼將繼續運行,但會收到 OperationCanceledException 或從中衍生的例外狀況。 程式碼在透過WaitWaitAll等方法同步等候工作時被封鎖,但可以繼續執行並進入例外狀況。
如果取消權杖在接受該權杖的 TAP 方法被呼叫前就請求取消,TAP 方法應該會回傳任務 Canceled 。 不過,如果在非同步作業執行時要求取消,則非同步作業不需要接受取消要求。 只有在作業因取消要求而結束時,傳回的工作才應該以狀態結束 Canceled 。 如果要求取消,但仍會產生結果或例外狀況,則工作應該以 或 RanToCompletionFaulted 狀態結束。
對於想要優先提供取消功能的非同步方法,您不需要提供不接受取消權杖的多載版本。 對於無法取消的方法,不要提供接受取消令牌的多載;這有助於向呼叫端指出目標方法是否實際可取消。 不希望取消的消費者程式碼可以呼叫一個方法,該方法接受 CancellationToken 並提供 None 作為參數值。 None 在功能上等同於預設 CancellationToken的 。
進度報告(可選)
某些非同步操作會受益於提供進度通知。 通常,利用這些通知更新使用者介面,提供非同步操作進度資訊。
在 TAP 中,透過 IProgress<T> 介面來處理進度。 將此介面作為參數傳遞給非同步方法,通常稱為 progress。 當你在呼叫非同步方法時提供進度介面,就能幫助消除因錯誤使用而產生的競賽狀況。 這些競賽條件發生在操作開始後事件處理程序錯誤註冊,導致錯過更新。 更重要的是,進度介面支援不同的進度實作,由取用程式代碼決定。 例如,消費程式碼可能只關心最新的進度更新,或想要緩衝所有更新、為每次更新呼叫動作,或控制呼叫是否會被編入特定執行緒。 所有這些選項皆可透過不同介面實作,並依消費者需求客製化。 與取消一樣,只有在 API 支援進度通知時,TAP 實作才應該提供 IProgress<T> 參數。
例如,若 ReadAsync 本文前述方法能以目前讀取的位元組數回報中間進度,則進度回調可以是一個 IProgress<T> 介面:
public static Task ReadAsync(byte[] buffer, int offset, int count,
IProgress<long> progress)
Public Function ReadAsync(buffer As Byte(), offset As Integer, count As Integer,
progress As IProgress(Of Long)) As Task
如果方法傳回符合特定搜尋模式之所有檔案清單,進度 FindFilesAsync 回呼可以提供已完成工作百分比的估計值,以及目前部分結果集。 它可以使用元組提供此資訊:
public static Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
string pattern,
IProgress<Tuple<double, ReadOnlyCollection<List<FileInfo>>>> progress)
Public Function FindFilesAsync(
pattern As String,
progress As IProgress(Of Tuple(Of Double, ReadOnlyCollection(Of List(Of FileInfo))))) As Task(Of ReadOnlyCollection(Of FileInfo))
或者使用適用於 API 的資料型別:
public static Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
string pattern,
IProgress<FindFilesProgressInfo> progress)
Public Function FindFilesAsync(
pattern As String,
progress As IProgress(Of FindFilesProgressInfo)) As Task(Of ReadOnlyCollection(Of FileInfo))
在後一種情況下,特殊資料類型通常以 為 ProgressInfo後綴。
若 TAP 實作提供接受 progress 參數的超載,則必須允許參數為 null。 如果你通過 null,則不會報告任何進展。 TAP 實作應該同步向物件報告 Progress<T> 進度,這可讓非同步方法快速提供進度。 它還允許資訊使用者決定如何以及在哪裡以最佳方式處理資訊。 例如,進度實例可以選擇管理回呼,並在已捕獲的同步上下文上引發事件。
IProgress<T> 實現方式
.NET 提供 Progress<T> 類別,該類別實作 IProgress<T>。 該 Progress<T> 類別宣告如下:
public class Progress<T> : IProgress<T>
{
public Progress();
public Progress(Action<T> handler);
protected virtual void OnReport(T value);
public event EventHandler<T>? ProgressChanged;
}
Progress<T> 的實例會公開 ProgressChanged 事件,每次非同步作業報告進度更新時都會引發此事件。 ProgressChanged事件在SynchronizationContext物件上觸發,是由Progress<T>實例在實例化時捕捉到的。 若無同步上下文,則使用針對執行緒池的預設上下文。 你可以在這個活動中註冊訓練員。 你也可以提供一個處理器給 Progress<T> 建構子,這樣會比較方便。 這個處理器的行為就像對 ProgressChanged 事件的事件處理器一樣。 進度更新會以非同步方式引發,以避免在事件處理常式執行時延遲非同步作業。 另一個 IProgress<T> 實作可以選擇套用不同的語意。
選擇要提供的多載
如果 TAP 實作同時使用選擇性參數 CancellationToken 和 IProgress<T>,則最多可能需要四個多載:
public Task MethodNameAsync(…);
public Task MethodNameAsync(…, CancellationToken cancellationToken);
public Task MethodNameAsync(…, IProgress<T> progress);
public Task MethodNameAsync(…,
CancellationToken cancellationToken, IProgress<T> progress);
Public MethodNameAsync(…) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken cancellationToken) As Task
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken,
progress As IProgress(Of T)) As Task
不過,許多 TAP 實作不提供取消或進度功能,因此需要單一方法:
public Task MethodNameAsync(…);
Public MethodNameAsync(…) As Task
如果 TAP 實作支援取消或進度,但不支援兩者,可能會提供兩種重載:
public Task MethodNameAsync(…);
public Task MethodNameAsync(…, CancellationToken cancellationToken);
// … or …
public Task MethodNameAsync(…);
public Task MethodNameAsync(…, IProgress<T> progress);
Public MethodNameAsync(…) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken) As Task
' … or …
Public MethodNameAsync(…) As Task
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task
如果 TAP 實作同時支援取消與進展,可能會暴露出所有四種超載。 然而,它可能只提供以下兩種:
public Task MethodNameAsync(…);
public Task MethodNameAsync(…,
CancellationToken cancellationToken, IProgress<T> progress);
Public MethodNameAsync(…) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken,
progress As IProgress(Of T)) As Task
為了彌補缺少的兩個中間組合,開發者可以使用 None 或 CancellationToken 作為 cancellationToken 參數的預設值,並使用 null 作為 progress 參數的預設值。
如果你期望每次使用 TAP 方法都能支援取消或進度,可以省略那些不接受相關參數的超載。
如果你決定提供多個重載版本以使取消或進度成為選項,那些不支援取消或進度的重載應該表現得像是將取消參數傳遞 None 或將進度參數傳遞 null 到支援這些參數的重載。
相關文章
- 非同步程式設計模式 — 介紹三種執行非同步操作的模式:基於任務的非同步模式(TAP)、非同步程式模型(APM)及事件基礎非同步模式(EAP)。
- 實作基於任務的非同步模式 — 描述如何以三種方式實作 TAP:使用 C# 與 Visual Basic 編譯器Visual Studio、手動,或結合編譯器與手動方法。
- 使用任務型非同步模式 — 說明如何透過任務和回調達到等待而不阻塞。
- 與其他非同步模式及類型互通 — 說明如何利用TAP實作非同步程式設計模型(APM)及事件基礎非同步模式(EAP)。