.NET 中的工作架構非同步模式 (TAP):簡介與概觀

在 .NET 中,工作架構非同步模式是新開發的建議非同步設計模式。 其是以 System.Threading.Tasks 命名空間中的 TaskTask<TResult> 類型為基礎,這兩種類別用於表示非同步作業。

命名、參數和傳回型別

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 或自訂資料結構來容納多個值。 即使 TAP 方法的同步對應項目沒有提供 CancellationToken 參數,也請考慮新增一個。

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

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

啟始非同步作業

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

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

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

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

例外狀況

非同步方法應只有在回應使用方式錯誤時,才引發從非同步方法呼叫擲回例外狀況。 使用方式錯誤一律不應發生在實際執行程式碼中。 例如,如果傳遞 Null 參考 (在 Visual Basic 中為 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> 衍生的類型隱密案例,以及支援分隔建構與排程,Task 類別會公開 Start 方法。 由公用 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 方法的每一種用法都支援取消或進度,可以省略不接受相關參數的多載。

如果您決定公開多個多載,使取消或進度成為選擇項,則不支援取消或進度的多載行為應該就像已針對取消傳遞 None 或針對進度傳遞 null 至支援兩者的多載。

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