.NET 中以工作為基礎的非同步模式 (TAP) :簡介和概觀

在 .NET 中,以工作為基礎的非同步模式是新開發的建議非同步設計模式。 它是以 命名空間中的 System.Threading.TasksTask<TResult> 型別為基礎 Task ,用來表示非同步作業。

命名、參數和傳回型別

TAP 使用單一方法表示非同步作業的啟始和完成。 這與非同步程式設計模型 (APM 或 IAsyncResult) 模式和事件架構非同步模式 (EAP) 形成對比。 APM 要求 BeginEnd 方法。 EAP 需要一個具有 Async 尾碼的方法,也需要一或多個事件、事件處理常式委派類型,以及 EventArg 衍生型別。 TAP 中的非同步方法會在傳回可等候型別之方法的作業名稱後面包括 Async 尾碼,例如 TaskTask<TResult>ValueTaskValueTask<TResult>。 例如,GetAsync 表示傳回 Task<String> 的非同步 Get 作業。 如果您將 TAP 方法加入至已包含具有 Async 尾碼之 EAP 方法名稱的類別,請改用 TaskAsync 尾碼。 例如,如果類別已經有 GetAsync 方法,請使用 GetTaskAsync 這個名稱。 如果方法啟動非同步作業但不傳回可等候型別,則其名稱應以 BeginStart 或其他動詞開頭,以表明此方法不傳回或擲回作業的結果。  

TAP 方法會傳回 System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult>,取決於對應的同步方法傳回 void 或 TResult 類型。

TAP 方法的參數應該與其同步對應項目的參數相符,並且應該以相同順序提供。 不過,outref 參數不受限於這項規則,因此應完全避免使用。 所有可能會透過 outref 參數傳回的資料,都應該改成做為 TResult 所傳回 Task<TResult> 的一部分傳回,而且應使用 Tuple 或自訂資料結構來容納多個值。 此外,請考慮新增 CancellationToken 參數,即使 TAP 方法的同步對應專案沒有提供參數也一樣。

專門用於建立、管理或組合工作的方法 (其中方法的非同步用意以方法名稱或方法所屬的類型名稱清楚表示) 不需要遵循這個命名模式,這類方法通常稱為「組合器」。 組合器的範例包括 WhenAllWhenAny,並且將在使用以工作為基礎的非同步模式文件的使用內建工作式組合器一節中加以討論。

如需說明 TAP 語法與舊有非同步程式設計模式 (例如非同步程式設計模型 (APM) 和事件式非同步模式 (EAP)) 的語法之間差異的範例,請參閱非同步程式設計模式

啟始非同步作業

以 TAP 為基礎的非同步方法可以先同步處理少量供作,例如驗證引數和啟始非同步作業,再傳回產生的工作。 同步工作量應盡量維持最少,這樣非同步方法才可以快速傳回。 快速傳回的原因包括:

  • 非同步方法可能是從使用者介面 (UI) 執行緒叫用,而任何長時間執行的同步工作都可能影響應用程式的回應。

  • 可能有多個非同步方法同時啟動。 因此,在非同步方法的同步處理部分中,所有長時間執行的工作都可能延遲啟始其他非同步作業,因而降低並行的優勢。

在某些情況下,完成作業所需的工作量會比以非同步方式啟動作業所需的工作量還少。 若資料流中的讀取作業可以藉由已在記憶體中緩衝的資料獲得滿足,則從該資料流進行讀取就是這類情境的範例。 在這類情況下,作業會同步完成,而且可能會傳回已完成的工作。

例外狀況

非同步方法應只有在回應使用方式錯誤時,才引發從非同步方法呼叫擲回例外狀況。 使用方式錯誤一律不應發生在實際執行程式碼中。 例如,如果在 Visual) Basic 中傳遞 null 參考 (Nothing ,因為其中一個方法的引數會導致錯誤狀態 (通常是以 ArgumentNullException 例外狀況) 表示,您可以修改呼叫程式碼,以確保永遠不會傳遞 Null 參考。 對於所有其他錯誤,非同步方法執行時發生的例外狀況應該指派給傳回的工作,即使非同步方法剛好在工作傳回前同步完成也一樣。 通常,工作最多只能包含一個例外狀況。 不過,如果工作表示多項作業 (例如 WhenAll),則可能會有多個例外狀況與單一工作相關聯。

目標環境

當您實作 TAP 方法時,可以判斷非同步執行發生的位置。 您可以選擇線上程集區上執行工作負載、使用非同步 I/O (來實作工作負載,而不需系結至執行緒,以取得大部分作業的執行) 、在特定執行緒上執行工作負載,例如 UI (執行緒) ,或使用任意數目的潛在內容。 TAP 方法甚至可能沒有可執行的項目,而且可能只傳回 Task,指出系統中其他位置發生的情形 (例如,代表資料的工作抵達佇列的資料結構)。

TAP 方法的呼叫端可能會透過同步等待產生的工作來阻止等待 TAP 方法完成,或是在非同步作業完成時執行其他 (接續) 程式碼。 接續程式碼 (Continuation Code) 的建立者可以控制執行程式碼的位置。 您可以明確建立接續程式碼、透過 Task 類別的方法建立 (例如 ContinueWith),或使用建置於接續之上的語言支援以隱含方式建立 (例如 C# 中的 await、Visual Basic 中的 Await,或 F# 中的 AwaitValue)。

工作狀態

Task 類別會提供非同步作業的生命週期,而該週期是以 TaskStatus 列舉表示。 為了支援衍生自 TaskTask<TResult> 的類型角落案例,以及為了支援從排程分離建構,類別 TaskStart 公開 方法。 由公用 Task 建構函式所建立的工作稱為「靜止工作」(Cold Task),因為這類工作的生命週期是從非排程的 Created 狀態開始,而且只有在這些執行個體上呼叫 Start 時才會排程。

所有其他工作都是從作用狀態開始其生命週期,也就是說,它們所代表的非同步作業已啟始,而且其工作狀態是 TaskStatus.Created 以外的列舉值。 從 TAP 方法傳回的所有工作都必須為啟用狀態。 如果 TAP 方法在內部使用工作的建構函式來具現化要傳回的工作,TAP 方法必須在物件上 Task 呼叫 Start ,才能傳回它。 TAP 方法的消費者可以安全地假設傳回的工作為作用中,並且不應嘗試在任何從 TAP 方法傳回的 Start 上呼叫 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 的參數傳遞至非同步方法。 在呼叫非同步方法時提供進度介面,有助於排除不當使用所造成的競爭情形 (也就是說,事件處理常式在作業啟動後才註冊是不正確的,而這樣可能導致遺失更新)。 更重要的是,進度介面支援各種不同的進度實作,取決於使用的程式碼。 例如,取用程式碼可能只關心最新的進度更新,或可能想要緩衝所有更新,或可能想要叫用每個更新的動作,或想要控制叫用是否封送至特定執行緒。 您可以使用介面的不同實作,根據特定取用者的需求來達成所有這些選項。 就像處理取消一樣,TAP 實作應只在 API 支援進度通知時才提供 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如果方法傳回符合特定搜尋模式的所有檔案清單,進度回呼可能會提供已完成工作百分比和目前部分結果集的估計。 它可以使用 Tuple 來提供這項資訊:

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 事件是在具現化 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 方法的每個用法都支援取消或進度,您可以省略不接受相關參數的多載。

如果您決定公開多個多載來進行取消或進度選擇性,不支援取消或進度的多載應該會像針對取消 null 或進度傳遞 None 至支援這些多載的進度一樣。

Title 描述
非同步程式設計模式 介紹執行非同步作業的三種模式:工作式非同步模式 (TAP)、非同步程式設計模型 (APM) 和事件式非同步模式 (EAP)。
實作以工作為基礎的非同步模式 描述三種實作工作式非同步模式 (TAP) 的方式:使用 Visual Studio 中的 C# 和 Visual Basic 編譯器、手動,或是透過編譯器和手動方法的組合。
使用以工作為基礎的非同步模式 描述如何使用工作和回呼達到等待的目的,而不會遭到封鎖。
Interop 與其他非同步模式和類型 描述如何使用工作式非同步模式 (TAP) 實作非同步程式設計模型 (APM) 和事件式非同步模式 (EAP)。