實現基於Task的異步模式


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

生成方法

編譯器生成

在.NET Framework 4.5中,C#編譯器實現了TAP。任何標有async關鍵字的方法都是異步方法,編譯器會使用TAP執行必要的轉換從而異步地實現方法。這樣的方法應該返回Task或者Task<TResult>類型。在后者的案例中,方法體應該返回一個TResult,且編譯器將確保通過返回的Task<TResult>是可利用的。相似地,方法體內未經處理的異常會被封送到輸出的task,造成返回的Task以Faulted的狀態結束。一個例外是如果OperationCanceledException(或派生類型)未經處理,那么返回的Task會以Canceled狀態結束。

手動生成

開發者可以手動地實現TAP,就像編譯器那樣或者更好地控制方法的實現。編譯器依賴來自System.Threading.Tasks命名空間暴露的公開表面區域(和建立在System.Threading.Tasks之上的System.Runtime.CompilerServices中支持的類型),還有對開發者直接可用的功能。當手動實現TAP方法時,開發者必須保證當異步操作完成時,完成返回的Task。

混合生成

在編譯器生成的實現中混合核心邏輯的實現,對於手動實現TAP通常是很有用的。比如這種情況,為了避免方法直接調用者產生而不是通過Task暴露的異常,如:

public Task<int> MethodAsync(string input)
{
    if (input == null) throw new ArgumentNullException("input");
    return MethodAsyncInternal(input);
}

private async Task<int> MethodAsyncInternal(string input)
{
    … // code that uses await
}

參數應該在編譯器生成的異步方法之外改變,這種委托有用的另一種場合是,當一個“快速通道”優化可以通過返回一個緩存的task來實現的時候。

工作負荷

計算受限和I/O受限的異步操作可以通過TAP方法實現。然而,當TAP的實現從一個庫公開暴露時,應該只提供給包含I/O操作的工作負荷(它們也可以包含計算,但不應該只包含計算)。如果一個方法純粹受計算限制,它應該只通過一個異步實現暴露,消費者然后就可以為了把該任務卸載給其他的線程的目的來選擇是否把那個同步方法的調用包裝成一個Task,並且/或者來實現並行。

計算限制

Task類最適合表示計算密集型操作。默認地,為了提供有效的執行操作,它利用了.Net線程池中特殊的支持,同時也對異步計算何時,何地,如何執行提供了大量的控制。

生成計算受限的tasks有幾種方法。

  1. 在.Net 4中,啟動一個新的計算受限的task的主要方法是TaskFactory.StartNew(),該方法接受一個異步執行的委托(一般來說是一個Action或者一個Func<TResult>)。如果提供了一個Action,返回的Task就代表那個委托的異步執行操作。如果提供了一個Func<TResult>,就會返回一個Task<TResult>。存在StartNew()的重載,該重載接受CancellationToken,TaskCreationOptions,和TaskScheduler,這些都對task的調度和執行提供了細粒度的控制。作用在當前調度者的工廠實例可以作為Task類的靜態屬性,例如Task.Factory.StartNew()。
  2. 在.Net 4.5中,Task類型暴露了一個靜態的Run方法作為一個StartNew方法的捷徑,可以很輕松地使用它來啟動一個作用在線程池上的計算受限的task。從.Net 4.5開始,對於啟動一個計算受限的task,這是一個更受人喜歡的機制。當行為要求更多的細粒度控制時,才直接使用StartNew。
  3. Task類型公開了構造函數和Start方法。如果必須要有分離自調度的構造函數,這些就是可以使用的(正如先前提到的,公開的APIs必須只返回已經啟動的tasks)。
  4. Task類型公開了多個ContinueWith的重載。當另外一個task完成的時候,該方法會創建新的將被調度的task。該重載接受CancellationToken,TaskCreationOptions,和TaskScheduler,這些都對task的調度和執行提供了細粒度的控制。
  5. TaskFactory類提供了ContinueWhenAll 和ContinueWhenAny方法。當提供的一系列的tasks中的所有或任何一個完成時,這些方法會創建一個即將被調度的新的task。有了ContinueWith,就有了對於調度的控制和任務的執行的支持。

思考下面的渲染圖片的異步方法。task體可以獲得cancellation token為的是,當渲染發生的時候,如果一個撤銷請求到達后,代碼可能過早退出。而且,如果一個撤銷請求在渲染開始之前發生,我們也可以阻止任何的渲染。

public Task<Bitmap> RenderAsync(
    ImageData data, CancellationToken cancellationToken)
{
    return Task.Run(() =>
    {
        var bmp = new Bitmap(data.Width, data.Height);
        for(int y=0; y<data.Height; y++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            for(int x=0; x<data.Width; x++)
            {
                … // render pixel [x,y] into bmp
            }
        }
        return bmp;
    }, cancellationToken);
}

如果下面的條件至少一個是正確的,計算受限的tasks會以一個Canceled狀態的結束:

  1. 在Task過度到TaskStatus.Running狀態之前,CancellationToken為一個發出撤銷請求的創建方法的參數提供(如StartNew,Run)。
  2. 有這樣的一個Task,它內部有未處理的OperationCanceledException。該OperationCanceledException 包含和CancellationToken屬性同名的CancellationToken傳遞到該Task,且該CancellationToken已經發出了撤銷請求。

如果該Task體中有另外一個未經處理的異常,那么該Task就會以Faulted的狀態結束,同時在該task上等待的任何嘗試或者訪問它的結果都將導致拋出異常。

I/O限制

使用TaskCompletionSource<TResult>類型創建的Tasks不應該直接被全部執行的線程返回。TaskCompletionSource<TResult>暴露了一個返回相關的Task<TResult>實例的Task屬性。該task的生命周期通過TaskCompletionSource<TResult>實例暴露的方法控制,換句話說,這些實例包括SetResult, SetException, SetCanceled, 和它們的TrySet* 變量。

思考這樣的需求,創建一個在特定的時間之后會完成的task。比如,當開發者在UI場景中想要延遲一個活動一段時間時,這可能使有用的。.NET中的System.Threading.Timer類已經提供了這種能力,在一段特定時間后異步地調用一個委托,並且我們可以使用TaskCompletionSource<TResult>把一個Task放在timer上,例如:

public static Task<DateTimeOffset> Delay(int millisecondsTimeout)
{
    var tcs = new TaskCompletionSource<DateTimeOffset>();
    new Timer(self =>
    {
        ((IDisposable)self).Dispose();
        tcs.TrySetResult(DateTimeOffset.UtcNow);
    }).Change(millisecondsTimeout, -1);
    return tcs.Task;
}

在.Net 4.5中,Task.Delay()就是為了這個目的而生的。比如,這樣的一個方法可以使用到另一個異步方法的內部,以實現一個異步的輪訓循環:

public static async Task Poll(
    Uri url, 
    CancellationToken cancellationToken, 
    IProgress<bool> progress)
{
    while(true)
    {
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
        bool success = false;
        try
        {
            await DownloadStringAsync(url);
            success = true;
        }
        catch { /* ignore errors */ }
        progress.Report(success);
    }
}

沒有TaskCompletionSource<TResult>的非泛型副本。然而,Task<TResult>派生自Task,因而,泛型的TaskCompletionSource<TResult>可以用於那些 I/O受限的方法,它們都利用一個假的TResult源(Boolean是默認選擇,如果開發者關心Task向下轉型的Task<TResult>的消費者,那么可以使用一個私有的TResult類型)僅僅返回一個Task。比如,開發的之前的Delay方法是為了順着產生的Task<DateTimeOffset>返回當前的時間。如果這樣的 一個結果值是不必要的,那么該方法可以通過下面的代碼取而代之(注意返回類型的改變和TrySetresult參數的改變):

public static Task Delay(int millisecondsTimeout)
{
    var tcs = new TaskCompletionSource<bool>();
    new Timer(self =>
    {
        ((IDisposable)self).Dispose();
        tcs.TrySetResult(true);
    }).Change(millisecondsTimeout, -1);
    return tcs.Task;
}

混合計算限制和I/O限制的任務

異步方法不是僅僅受限於計算受限或者I/O受限的操作,而是可以代表這兩者的混合。實際上,通常情況是不同性質的多個異步操作被組合在一起生成更大的混合操作。比如,思考之前的RenderAsync方法,該方法基於一些輸入的ImageData執行一個計算密集的操作來渲染一張圖片。該ImageData可能來自於一個我們異步訪問的Web服務:

public async Task<Bitmap> DownloadDataAndRenderImageAsync(
    CancellationToken cancellationToken)
{
    var imageData = await DownloadImageDataAsync(cancellationToken);
    return await RenderAsync(imageData, cancellationToken);
}

這個例子也展示了一個單獨的CancellationToken是如何通過多個異步操作被線程化的。

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



免責聲明!

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



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