基於Task的異步模式的定義


返回該系列目錄《基於Task的異步模式--全面介紹》

命名,參數和返回類型

在TAP(Task-based Asynchronous Pattern)中的異步操作的啟動和完成是通過一個單獨的方法來表現的,因此只有一個方法要命名。這與IAsyncResult模式或者APM(Asynchronous Programming Model,異步編程模型)模式形成對比,后者必須要有開始方法名和結束方法名;還與基於事件(event-based)的異步模式(EAP)不同,它們要求方法名以Async為后綴,而且要求一個或多個事件,事件句柄委托類型和派生自Event參數的類型。TAP中的異步方法使用“Async”后綴命名,跟在操作名稱的后面(例如MethodNameAsync)。TAP中的異步方法返回一個Task類型或者Task<TResult>,基於相應的同步方法是否分別返回一個void或者TResult類型。

比如,思考下面的“Read”方法,它將特定數量的數據讀取到一個以特定偏移量的buffer中:

public class MyClass
{
    public int Read(byte [] buffer, int offset, int count);
}

這個方法對應的APM版本則有下面兩個方法:

public class MyClass
 {
    public IAsyncResult BeginRead(byte[] buffer, int offset, int count,AsyncCallback callback, object state);
    public int EndRead(IAsyncResult asyncResult);
}

EAP版本對應的方法是這樣的:

public class MyClass
{
    public void ReadAsync(byte[] buffer, int offset, int count);
    public event ReadCompletedEventHandler ReadCompleted;
}
public delegate void ReadCompletedEventHandler(object sender, ReadCompletedEventArgs eventArgs);
public class ReadCompletedEventArgs: AsyncCompletedEventArgs
{
    public int Result {
        get;
    }
}

TAP對應的版本只有下面一個方法:

public class MyClass
{
    public Task<int> ReadAsync(byte [] buffer, int offset, int count);
}

一個基本的TAP方法的參數應該和同步方法的參數相同,且順序相同。然而,“out”和“ref”參數不遵從這個規則,並且應該避免使用它們。通過out或者ref返回的任何數據可以作為返回的Task<TResult>結果的一部分,可以利用一個元組或者一個自定義數據結構容納多個值。

純粹致力於創建,操作,或組合的任務方法(該方法的異步目的在方法名上或者在方法上以類型命名是明確的)不需要遵循上述命名模式;這些方法通常被稱為"組合子"。這種方法的例子包括Task. WhenAll和Task.WhenAny,本文檔后面的會更深入地討論。

表現

初始化異步操作

在返回結果的任務之前,基於TAP異步方法允許同步地處理少量的工作。這項工作應保持在所需的最低數量,執行如驗證參數和啟動異步操作的操作。很可能從用戶界面線程將調用異步方法,因此所有長時間運行的異步方法的同步前期部分工作可能會損害響應能力。很有可能同時將啟動多個異步方法,因此所有長時間運行的異步方法的同步前期部分工作可能會推遲啟動其他異步操作,從而減少並發的好處。

在某些情況下,完成操作所需的工作量小於異步啟動操作需要的工作量(例如,從流中讀取數據,這個讀取操作可以被已經緩沖在內存中的數據所滿足)。在這種情況下,操作可能同步完成,返回一個已經完成的任務。

異常

一個異步方法只應該直接捕獲一個MethodNameAsync 調用時拋出的異常以響應用法錯誤。對於其他所有的錯誤,在異步方法執行期間發生的異常應該分配給返回的任務。這種情況是在Task返回之前,異步方法同步完成下發生的。一般地,一個Task至多包含一個異常。然而,對於一個Task表示多個操作(如,Task.WhenAll)的情況,單個Task也會關聯多個異常。

【*每個.Net設計指南都指出,一個用法錯誤可以通過改變調用方法的碼來避免。比如,當把null作為一個方法的參數傳遞時,錯誤狀態就會發生,錯誤條件通常被表示為ArgumentNullException,開發者可以修改調用碼來確保null沒有傳遞過。換言之,開發者可以並且應該確保用法錯誤從來沒有在生產代碼中發生過】。

目標環境

異步執行的發生取決於TAP方法的實現。TAP方法的開發人員可能選擇在線程池上執行工作負載,也可能選擇使用異步 I/O實現它,因而沒有被綁定到大量操作執行的線程上,也可以選擇在特定的線程上運行,如UI線程,或者其他一些潛在的上下文。甚至可能是這種情況,TAP方法沒有東西執行,簡單返回一個在系統中其他地方情況發生的Task(如Task<TData>表示TData到達一個排隊的數據結構)。

TAP方法的調用者也可能阻塞等待TAP方法的完成(通過在結果的Task上同步地等待),或者利用延續在異步操作完成時執行附加代碼。延續創建者在延續代碼執行的地方有控制權。這些延續代碼要么通過Task類(如ContinueWith)顯示地創建,要么使用語言支持隱式地建立在延續代碼之上(如C#中的“await”)。

Task狀態

Task類提供了異步操作的生命周期,該生命周期通過TaskStatus枚舉表示。為了支持派生自Task和Task<TResult>類型的案例,以及來自調度的構建分離,Task類暴露了一個Start方法。通過public構造函數創建的Tasks被稱為“冷”任務,在“冷”任務中,它們以非調度(non-scheduled)的TaskStatus.Created狀態開始生命周期。直到在這些實例上Start調用時,它們才促使被調度。所有在“熱”狀態開始生命周期的其他task,意味着它們表示的異步操作已經初始化了,它們的TaskStatus是一個除Created之外的其它枚舉值。

所有從TAP方法返回的tasks肯定是“熱的”。如果TAP方法內部使用一個Task的構造函數來實例化要返回的task,那么此TAP方法必須在返回task之前在Task對象上調用Start方法。TAP方法的消費者可以安全地假定返回的task是“熱的”,並不應該嘗試在任何返回自TAP方法的Task上調用Start。在“熱的”task上調用Start會導致InvalidOperationException (Task類自動處理這個檢查)。

可選:撤銷

TAP中的撤銷對於異步方法的實現者和異步方法的消費者都是選擇加入的。如果一個操作將要取消,那么它會暴露

一個接受System.Threading.CancellationToken的MethodNameAsync 的重載。異步操作會監視對於撤銷請求的這個token,如果接收到了撤銷請求,可以選擇處理該請求並取消操作。如果處理請求導致任務過早地結束,那么從TAP方法返回的Task會以TaskStatus.Canceled狀態結束。

為了暴露一個可取消的異步操作,TAP實現提供了在同步對應的方法的參數后接受一個CancellationToken的重載。按照慣例,該參數命名為“cancellationToken”。

public Task<int> ReadAsync(
    byte [] buffer, int offset, int count, 
    CancellationToken cancellationToken);

如果token已經請求了撤銷並且異步操作尊重該請求,那么返回的task將會以TaskStatus.Canceled狀態結束,將會產生沒有可利用的Result,並且沒有異常。Canceled狀態被認為是一個伴隨着Faulted和RanToCompletion 狀態的任務最終或完成的狀態。因此,Canceled 狀態的task的IsCompleted 屬性返回true。當一個Canceled 狀態的task完成時,任何用該task注冊的延續操作都會被調度或執行,除非這些延續操作通過具體的TaskContinuationOptions 用法在被創建時取消了(如TaskContinuationOptions.NotOnCanceled)。任何異步地等待一個通過語言特性使用的撤銷的task的代碼將會繼續執行並且收到一個OperationCanceledException(或派生於該異常的類型)。在該task(通過Wait 或WaitAll方法)上同步等待而阻塞的任何代碼也會繼續執行並拋出異常。

如果CancellationToken已經在接受那個token的TAP方法調用之前發出了取消請求,那么該TAP方法必須返回一個Canceled狀態的task。然而,如果撤銷在異步操作執行期間請求,那么異步操作不需要尊重該撤銷請求。只有由於撤銷請求的操作完成時,返回的Task才會以Canceled 狀態結束。如果一個撤銷被請求了,但是結果或異常仍產生了,那么Task將會分別以RanToCompletion或 Faulted 的狀態結束。

首先,在使用異步方法的開發者心目中,那些渴望撤銷的方法,需要提供一個接受CancellationToken變量的重載。對於不可取消的方法,不應該提供接受CancellationToken的重載。這個有助於告訴調用者目標方法實際上是否是可取消的。不渴望撤銷的消費者可以調用一個接受CancellationToken的方法來把CancellationToken.None作為提供的參數值。CancellationToken.None功能上等價於default(CancellationToken)。

可選:進度報告

一些異步操作得益於提供的進度通知,一般利用這些進度通知來更新關於異步操作進度的UI。

在TAP中,進度通過IProgress<T>接口傳遞給異步方法的名為“progress”的參數來處理。在該異步方法調用時提供這個進度接口有助於消除來自於錯誤的用法的競爭條件,這些錯誤的用法 是因為在此操作可能錯過更新之后,事件句柄錯誤地注冊導致的。更重要的是,它使變化的進度實現可被利用,因為由消費者決定。比如,消費者肯僅僅關心最新的進度更新,或者可能緩沖所有更新,或者可能僅僅想要為每個更新調用一個action,或者可能想控制是否調用封送到特定的線程。所有這些可能通過使用一個不同的接口的實現來完成,每一個接口可以定制到特殊的消費者需求。因為有了撤銷,如果API支持進度通知,那么TAP實現應該只提供一個IProgress<T>參數。

比如,如果我們上面提到的ReadAsync方法可以以迄今讀取字節數的形式能報告中間的進度,那么進度的回調(callback)可以是一個IProgress<int>:

public Task<int> ReadAsync(
    byte [] buffer, int offset, int count, 
    IProgress<int> progress);

如果FindFilesAsync方法返回一個所有文件的列表,該列表滿足一個特殊的搜索模式,那么進度回調可以提供完成工作的百分比和當前部分結果集的估計。它也可以這樣處理元組,如:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
    string pattern, 
    IProgress<Tuple<double,ReadOnlyCollection<List<FileInfo>>>> progress);

或者使用API具體的數據類型,如:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
    string pattern, 
    IProgress<FindFilesProgressInfo> progress);

在后一種情況,特殊的數據類型以“ProgressInfo”為后綴。

如果TAP實現提供了接受progress參數的重載,那么它們必須允許參數為null,為null的情況下,進度不會報告。TAP實現應該同步地報告IProgress<T>對象的進度,使得比快速提供進度的異步實現更廉價,並且允許進度的消費者決定如何以及在哪里最好地處理信息(例如進度實例本身可以選擇在一個捕獲的同步上下文上收集回調函數和引發事件)。

IProgreee<T>實現

Progress<T>作為.NET Framework 4.5的一部分,是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事件,它是每次異步操作報告進度更新的時候觸發。當Progress<T>實例被實例化時,該事件在被捕獲的同步上下文上觸發(如果沒有上下文可用,那么用默認的線程池上下文)。句柄可能會用這個事件注冊;一個單獨的句柄也可能提供給Progress實例的構造函數(這純粹是為了方便,就像ProgressChanged 事件的事件句柄)。進度更新異步觸發是為了事件句柄執行時避免延遲異步操作。其他的IProgress<T>實現可能選擇使用了不同的語義。

如何選擇提供的重載函數

有了CancellationToken和IProgress<T>參數,TAP的實現默認有4個重載函數:

public Task MethodNameAsync(…);
public Task MethodNameAsync(…, CancellationToken cancellationToken);
public Task MethodNameAsync(…, IProgress<T> progress); 
public Task MethodNameAsync(…, 
    CancellationToken cancellationToken, IProgress<T> progress);

然而,因為它們沒有提供cancellation和progress的能力,許多TAP實現有了最短的重載的需求:

public Task MethodNameAsync(…);

如果一個實現支持cancellation或者progress但不同時支持,那么TAP實現可以提供2個重載:

public Task MethodNameAsync(…);
public Task MethodNameAsync(…, CancellationToken cancellationToken);

// … or …

public Task MethodNameAsync(…);
public Task MethodNameAsync(…, IProgress<T> progress);

如果實現同時支持cancellation和progress,那么它可以默認提供4個重載。然而,只有2個有效:

public Task MethodNameAsync(…);
public Task MethodNameAsync(…, 
    CancellationToken cancellationToken, IProgress<T> progress);

為了得到那2個遺失的重載,開發者可以通過給CancellationToken參數傳遞CancellationToken.None(或者default(CancellationToken))和/或給progress參數傳遞null。

如果期望TAP方法的每一種用法都應該使用cancellation和/或progress,那么不接受相關參數的重載可以忽略。

如果TAP方法的多個重載公開了可選的cancellation和/或progress,那么不支持cancellation和/或progress的重載的表現應該像支持他們的重載已經傳遞了CancellationToken.None和null分別給cancellation和progress一樣。

返回該系列目錄《基於Task的異步模式--全面介紹》



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM