在.NET中,基於任務的非同步模式是新開發推薦的非同步設計模式。 它以在System.Threading.Tasks命名空間中的Task和Task<TResult>類型為基礎,用於表示非同步作業。
命名、參數和傳回類型
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 或類型 TResult, 回傳 System.Threading.Tasks.Task 或 System.Threading.Tasks.Task<TResult>。
TAP 方法的參數應該符合其同步對應方法的參數,而且應該以相同的順序提供。 但是, out 參數 ref 不受此規則的約束,應完全避免。 任何透過out或ref參數傳回的資料都應該被視為由Task<TResult>所傳回TResult的一部分,並且應該使用元組或自訂資料結構來容納多個值。 此外,即使 TAP 方法的同步對應專案未提供參數,也請考慮新增 CancellationToken 參數。
專門用於工作建立、操作或組合的方法 (其中方法的非同步意圖在方法名稱或方法所屬類型的名稱中很清楚) 不需要遵循此命名模式;此類方法通常稱為 組合器。 組合器的範例包括 WhenAll 和 WhenAny,並在使用以任務為基礎的非同步模式一文的使用內建任務型組合器一節中討論。
如需 TAP 語法與舊版非同步程式設計模式 (例如非同步程式設計模型 (APM) 和事件型非同步模式 (EAP) ) 中使用的語法有何不同的範例,請參閱 非同步程式設計模式。
起始非同步作業
以 TAP 為基礎的非同步方法可以在傳回產生的工作之前,同步執行少量工作,例如驗證引數和起始非同步作業。 同步工作應該保持在最小值,以便非同步方法可以快速返回。 快速退貨的原因包括:
非同步方法可能會從使用者介面 (UI) 執行緒叫用,而任何長時間執行的同步工作都可能損害應用程式的回應能力。
可以同時啟動多個非同步方法。 因此,非同步方法同步部分中的任何長時間執行工作都可能延遲其他非同步作業的起始,從而減少並行的好處。
在某些情況下,完成作業所需的工作量小於非同步啟動作業所需的工作量。 從資料流讀取作業若可由已緩衝在記憶體中的資料滿足,就屬於這類案例。 在這種情況下,操作可能會同步完成,並且可能會傳回已完成的工作。
Exceptions
非同步方法應該僅在用法錯誤時才引發例外狀況,從非同步方法呼叫中丟出。 生產程式碼中不應發生使用錯誤。 例如,如果傳遞 Null 參考 (Nothing 在 Visual Basic 中) 作為方法的其中一個引數會導致錯誤狀態 (通常以例外狀況表示 ArgumentNullException ),您可以修改呼叫程式碼,以確保永遠不會傳遞 Null 參考。 對於所有其他錯誤,非同步方法執行時發生的例外狀況應該指派給傳回的工作,即使非同步方法恰好在傳回工作之前同步完成也一樣。 一般而言,工作最多包含一個例外狀況。 不過,如果任務代表多個作業 (例如, WhenAll),多個例外狀況可能與單一任務相關聯。
目標環境
當您實作 TAP 方法時,您可以判斷非同步執行發生的位置。 您可以選擇在執行緒集區上執行工作負載,使用非同步 I/O 來實作工作負載(大部分作業執行不需綁定至某個執行線程),在特定執行緒上執行工作負載(例如 UI 執行緒),或使用任意數目的不同執行環境。 TAP 方法甚至可能沒有任何要執行的內容,並可能僅傳回一個 Task,這表示系統中其他位置發生的某種狀況(例如,一個代表資料到達佇列資料結構的工作)。
TAP 方法的呼叫端可能會透過同步等候產生的工作來封鎖等候 TAP 方法完成,或在非同步作業完成時執行其他 (接續) 程式代碼。 接續程式碼的建立者可以控制該程式碼的執行位置。 您可以透過類別上 Task 的方法 (例如 ContinueWith) 明確地建立接續程式碼,也可以使用建置在接續之上的語言支援 (例如, await 在 C#、 Await Visual Basic 中、 AwaitValue F#) 以隱含方式建立接續程式碼。
任務狀態
類別 Task 提供非同步作業的生命週期,該週期由列舉 TaskStatus 表示。 為了支援由 Task 和 Task<TResult> 衍生的類型的極端情況,以及支援建構與排程的分離,類別 Task 提供了 Start 方法。 由公用 Task 建構函式建立的任務稱為 冷任務,因為它們的生命週期在非排程 Created 狀態下開始,只有當在這些實例上呼叫 Start 時才會進入排程狀態。
所有其他作業都會以熱狀態開始其生命週期,這表示它們所代表的非同步作業已起始,且其作業狀態是TaskStatus.Created以外的列舉值。 必須啟動從 TAP 方法傳回的所有作業。 如果 TAP 方法在內部使用工作的建構函式來具現化要傳回的工作,則 TAP 方法必須於 Task 物件上呼叫 Start,才能傳回它。 TAP 方法的取用者可以安全地假設傳回的工作是作用中,而且不應該嘗試呼叫 Start 從 TAP 方法傳回的任何 Task 工作。 呼叫作用中工作Start,會導致InvalidOperationException例外狀況。
取消(可選)
在 TAP 中,非同步方法實作者和非同步方法取用者都可以選擇取消。 如果作業允許取消,它會公開一個接受取消權杖(CancellationToken 實例)的非同步方法的多載。 依慣例,參數會命名 cancellationToken為 。
public 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 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 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 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,在此情況下,不會報告任何進度。 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事件會在Progress<T>實例被實例化時擷取的SynchronizationContext物件上引發。 如果沒有可用的同步化環境定義,則會使用以執行緒集區為目標的預設環境定義。 處理程序可能會登記到此事件上。 為了方便起見,也可以將單一處理常式提供給 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 作為進度傳遞至支援這些功能的多載。
相關文章
| Title | Description |
|---|---|
| 非同步程式設計模式 | 介紹執行非同步作業的三種模式:以工作為基礎的非同步模式 (TAP)、非同步程式設計模型 (APM) 和以事件為基礎的非同步模式 (EAP)。 |
| 實作以工作為基礎的非同步模式 | 說明如何以三種方式實作工作型非同步模式 (TAP):在 Visual Studio 中使用 C# 和 Visual Basic 編譯器、手動或透過編譯器和手動方法的組合。 |
| 使用以任務為基礎的非同步模式 | 描述如何使用任務和回調來實現不阻塞的等待。 |
| 與其他非同步模式和類型的互操作 | 說明如何使用工作型非同步模式 (TAP) 來實作非同步程式設計模型 (APM) 和事件型非同步模式 (EAP)。 |