c# 幾個常見的TAP異步操作

在本系列上一篇文章 [15:異步編程基礎] 中,我們講到,現代應用程序廣泛使用的是基於任務的異步編程模式(TAP),歷史的 EAP 和 AMP 模式已經過時不推薦使用。今天繼續總結一下 TAP 的異步操作,比如取消任務、報告進度、Task.Yield()、ConfigureAwait() 和並行操作等。

雖然實際 TAP 編程中很少使用到任務的狀態,但它是很多 TAP 操作機理的基礎,所以下面先從任務狀態講起。

1 任務狀態

Task 類為異步操作提供瞭一個生命周期,這個周期由 TaskStatus 枚舉表示,它有如下值:

public enum TaskStatus
{
    Created = 0,
    WaitingForActivation = 1,
    WaitingToRun = 2,
    Running = 3,
    WaitingForChildrenToComplete = 4,
    RanToCompletion = 5,
    Canceled = 6,
    Faulted = 7
}

其中 Canceled、Faulted 和 RanToCompletion 狀態一起被認為是任務的最終狀態。因此,如果任務處於最終狀態,則其 IsCompleted 屬性為 true 值。

手動控制任務啟動

為瞭支持手動控制任務啟動,並支持構造與調用的分離,Task 類提供瞭一個 Start 方法。由 Task 構造函數創建的任務被稱為冷任務,因為它們的生命周期處於 Created 狀態,隻有該實例的 Start 方法被調用才會啟動。

任務狀態平時用的情況不多,一般我們在封裝一個任務相關的方法時,可能會用到。比如下面這個例子,需要判斷某任務滿足一定條件才啟動:

static void Main(string[] args)
{
    MyTask t = new(() =>
    {
        // do something.
    });

    StartMyTask(t);

    Console.ReadKey();
}

public static void StartMyTask(MyTask t)
{
    if (t.Status == TaskStatus.Created && t.Counter>10)
    {
        t.Start();
    }
    else
    {
        // 這裡模擬計數,直到 Counter>10 再執行 Start
        while (t.Counter <= 10)
        {
            // Do something
            t.Counter++;
        }
        t.Start();
    }
}

public class MyTask : Task
{
    public MyTask(Action action) : base(action)
    {
    }

    public int Counter { get; set; }
}

同樣,TaskStatus.Created 狀態以外的狀態,我們叫它熱任務,熱任務一定是被調用瞭 Start 方法激活過的。

確保任務已激活

註意,所有從 TAP 方法返回的任務都必須被激活,比如下面這樣的代碼:

MyTask task = new(() =>
{
    Console.WriteLine("Do something.");
});

// 在其它地方調用
await task;

在 await 之前,任務沒有執行 Task.Start 激活,await 時程序就會一直等待下去。所以如果一個 TAP 方法內部使用 Task 構造函數來實例化要返回的 Task,那麼 TAP 方法必須在返回 Task 對象之前對其調用 Start。

2 任務取消

在 TAP 中,取消對於異步方法實現者和消費者來說都是可選的。如果一個操作允許取消,它就會暴露一個異步方法的重載,該方法接受一個取消令牌(CancellationToken 實例)。按照慣例,參數被命名為 cancellationToken。例如:

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

異步操作會監控這個令牌是否有取消請求。如果收到取消請求,它可以選擇取消操作,如下面的示例通過 while 來監控令牌的取消請求:

static void Main(string[] args)
{
    CancellationTokenSource source = new();
    CancellationToken token = source.Token;

    var task = DoWork(token);

    // 實際情況可能是在稍後的其它線程請求取消
    Thread.Sleep(100);
    source.Cancel();

    Console.WriteLine($"取消後任務返回的狀態:{task.Status}");

    Console.ReadKey();
}

public static Task DoWork(CancellationToken cancellationToken)
{
    while (!cancellationToken.IsCancellationRequested)
    {
        // Do something.
        Thread.Sleep(1000);

        return Task.CompletedTask;
    }
    return Task.FromCanceled(cancellationToken);
}

如果取消請求導致工作提前結束,甚至還沒有開始就收到請求取消,則 TAP 方法返回一個以 Canceled 狀態結束的任務,它的 IsCompleted 屬性為 true,且不會拋出異常。當任務在 Canceled 狀態下完成時,任何在該任務註冊的延續任務仍都會被調用和執行,除非指定瞭諸如 NotOnCanceled 這樣的選項來選擇不延續。

但是,如果在異步任務在工作時收到取消請求,異步操作也可以選擇不立刻結束,而是等當前正在執行的工作完成後再結束,並返回 RanToCompletion 狀態的任務;也可以終止當前工作並強制結束,根據實際業務情況和是否生產異常結果返回 Canceled 或 Faulted 狀態。

對於不能被取消的業務方法,不要提供接受取消令牌的重載,這有助於向調用者表明目標方法是否可以取消。

3 進度報告

幾乎所有異步操作都可以提供進度通知,這些通知通常用於用異步操作的進度信息更新用戶界面。

在 TAP 中,進度是通過 IProgress<T> 接口來處理的,該接口作為一個參數傳遞給異步方法。下面是一個典型的的使用示例:

static void Main(string[] args)
{
    var progress = new Progress<int>(n =>
    {
        Console.WriteLine($"當前進度:{n}%");
    });

    var task = DoWork(progress);

    Console.ReadKey();
}

public static async Task DoWork(IProgress<int> progress)
{
    for (int i = 1; i <= 100; i++)
    {
        await Task.Delay(100);
        if (i % 10 == 0)
        {
            progress?.Report(i);
        };
    }
}

輸出如下結果:

當前進度:10%
當前進度:20%
當前進度:30%
當前進度:40%
當前進度:50%
當前進度:60%
當前進度:70%
當前進度:80%
當前進度:90%
當前進度:100%

IProgress<T> 接口支持不同的進度實現,這是由消費代碼決定的。例如,消費代碼可能隻關心最新的進度更新,或者希望緩沖所有更新,或者希望為每個更新調用一個操作,等等。所有這些選項都可以通過使用該接口來實現,並根據特定消費者的需求進行定制。例如,如果本文前面的 ReadAsync 方法能夠以當前讀取的字節數的形式報告進度,那麼進度回調可以是一個 IProgress<long> 接口。

public Task ReadAsync(
    byte[] buffer, int offset, int count,
    IProgress<long> 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)

如果 TAP 的實現提供瞭接受 IProgress<T> 參數的重載,它們必須允許參數為空,在這種情況下,不會報告進度。IProgress<T> 實例可以作為獨立的對象,允許調用者決定如何以及在哪裡處理這些進度信息。

4 Task.Yield 讓步

我們先來看一段 Task.Yield() 的代碼:

Task.Run(async () =>
{
    for(int i=0; i<10; i++)
    {
        await Task.Yield();
        ...
    }
});

這裡的 Task.Yield() 其實什麼也沒幹,它返回的是一個空任務。那 await 一個什麼也沒做的空任務有什麼用呢?

我們知道,對計算機來說,任務調度是根據一定的優先策略來安排線程去執行的。如果任務太多,線程不夠用,任務就會進入排隊狀態。而 Yield 的作用就是讓出等待的位置,讓後面排除的任務先行。它字面上的意思就是讓步,當任務做出讓步時,其它任務就可以盡快被分配線程去執行。舉個現實生活中的例子,就像你在排隊辦理業務時,好不容易到你瞭,但你的事情並不急,自願讓出位置,讓其他人先辦理,自己假裝臨時有事到外面溜一圈什麼事也沒幹又回來重新排隊。默默地做瞭一次大善人。

Task.Yield() 方法就是在異步方法中引入一個讓步點。當代碼執行到讓步點時,就會讓出控制權,去線程池外面兜一圈什麼事也沒幹再回來重新排隊。

5 定制異步任務後續操作

我們可以對異步任務執行完成的後續操作進行定制。常見的兩個方法是 ConfigureAwait 和 ContinueWith。

ConfigureAwait

我們先來看一段 Windows Form 中的代碼:

private void button1_Click(object sender, EventArgs e)
{
    var content = CurlAsync().Result;
    ...
}

private async Task<string> CurlAsync()
{
    using (var client = new HttpClient())
    {
        returnawait client.GetStringAsync("http://geekgist.com");
    }
}

想必大傢都知道 CurlAsync().Result 這句代碼在 Windows Form 程序中會造成死鎖。原因是 UI 主線程執行到這句代碼時,就開始等待異步任務的結果,處於阻塞狀態。而異步任務執行完後回來準備找 UI 線程繼續執行後面的代碼時,卻發現 UI 線程一直處於“忙碌”的狀態,沒空搭理回來的異步任務。這就造成瞭你等我,我又在等你的尷尬局面。

當然,這種死鎖的情況隻會在 Winform 和早期的 ASP.NET WebForm 中才會發生,在 Console 和 Web API 應用中不會生產死鎖。

解決辦法很簡單,作為異步方法調用者,我們隻需改用 await 即可:

private async void button1_Click(object sender, EventArgs e)
{
    var content = await CurlAsync();
    ...
}

在異步方法內部,我們也可以調用任務的 ConfigureAwait(false) 方法來解決這個問題。如:

private async Task<string> CurlAsync()
{
    using (var client = new HttpClient())
    {
        returnawait client
            .GetStringAsync("http://geekgist.com")
            .ConfigureAwait(false);
    }
}

雖然兩種方法都可行,但如果作為異步方法提供者,比如封裝一個通用庫時,考慮到難免會有新手開發者會使用 CurlAsync().Result,為瞭提高通用庫的容錯性,我們就可能需要使用 ConfigureAwait 來做兼容。

ConfigureAwait(false) 的作用是告訴主線程,我要去遠行瞭,你去做其它事情吧,不用等我。隻要先確保一方不在一直等另一方,就能避免互相等待而造成死鎖的情況。

ContinueWith

ContinueWith 方法很容易理解,就是字面上的意思。作用是在異步任務執行完成後,安排後續要執行的工作。示例代碼:

private void Button1_Click(object sender, EventArgs e)
{
    var backgroundScheduler = TaskScheduler.Default;
    var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    Task.Factory
        .StartNew(_ => DoBackgroundComputation(), backgroundScheduler)
        .ContinueWith(_ => UpdateUI(), uiScheduler)
        .ContinueWith(_ => DoAnotherBackgroundComputation(), backgroundScheduler)
        .ContinueWith(_ => UpdateUIAgain(), uiScheduler);
}

如上,可以一直鏈式的寫下去,任務會按照順序執行,一個執行完再繼續執行下一個。若其中一個任務返回的狀態是 Canceled 時,後續的任務也將被取消。這個方法有好些個重載,在實際用到的時候再查看文檔即可。

6 總結

本文內容都是相對比較基礎的 TAP 異步操作知識點。C# 的 TAP 很強大,提供的 API 也很多,遠不止本文講的這些,都是圍繞 Task 轉的。關鍵是要理解好基礎操作,才能靈活使用更高級的功能。希望本文對你有所幫助。

以上就是c# 幾個常見的TAP異步操作的詳細內容,更多關於c# TAP異步操作的資料請關註WalkonNet其它相關文章!

推薦閱讀:

    None Found