【異步編程】Part3:取消異步操做

在.Net和C#中運行異步代碼至關簡單,由於咱們有時候須要取消正在進行的異步操做,經過本文,能夠掌握 經過CancellationToken取消任務(包括non-cancellable任務)。git

 早期

早期.Net 使用 BackgroundWorker 完成異步長時間運行操做。
能夠使用CacnelAsync方法設置 CancellationPending = true
private void BackgroundLongRunningTask(object sender, DoWorkEventArgs e)
{
    BackgroundWorker worker = (BackgroundWorker)sender;

    for (int i = 1; i <= 10000; i++)
    {
        if (worker.CancellationPending == true)
        {
            e.Cancel = true;
            break;
        }
        
        // Do something
    }
}
View Code

 已經再也不推薦這種方式來完成異步和長時間運行的操做,可是大部分概念在如今依舊能夠使用。github

 Task橫空出世

Task表明一個異步操做, 該類表示一個異步不返回值的操做, 泛型版本Task<TResult>表示異步有返回值的操做
可以使用async/await 語法糖代碼去完成異步操做。
 
如下建立一個簡單的長時間運行的操做:
/// <summary>
/// Compute a value for a long time.
/// </summary>
/// <returns>The value computed.</returns>
/// <param name="loop">Number of iterations to do.</param>
private static Task<decimal> LongRunningOperation(int loop)
{
    // Start a task and return it
    return Task.Run(() =>
    {
        decimal result = 0;

        // Loop for a defined number of iterations
        for (int i = 0; i < loop; i++)
        {
            // Do something that takes times like a Thread.Sleep in .NET Core 2.
            Thread.Sleep(10);
            result += i;
        }

        return result;
    });
}
// 這裏咱們使用Thread.Sleep 模仿長時間運行的操做
View Code

 簡單異步調用代碼:異步

public static async Task ExecuteTaskAsync()
{
    Console.WriteLine(nameof(ExecuteTaskAsync));
    Console.WriteLine("Result {0}", await LongRunningOperation(100));
    Console.WriteLine("Press enter to continue");
    Console.ReadLine();
}

敲黑板: C#取消異步操做分爲

 

① 讓代碼可取消(Cancellable)

由於一些緣由,長時間運行的操做花費了 冗長的時間(須要取消,避免佔用資源); 或者不肯意再等待執行結果了
咱們會取消異步操做。
 
爲完成目的須要在 長時間運行的異步任務中傳入CancellationToken:
/// <summary>
/// Compute a value for a long time.
/// </summary>
/// <returns>The value computed.</returns>
/// <param name="loop">Number of iterations to do.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private static Task<decimal> LongRunningCancellableOperation(int loop, CancellationToken cancellationToken)
{
    Task<decimal> task = null;

    // Start a task and return it
    task = Task.Run(() =>
    {
        decimal result = 0;

        // Loop for a defined number of iterations
        for (int i = 0; i < loop; i++)
        {
            // Check if a cancellation is requested, if yes,
            // throw a TaskCanceledException.

            if (cancellationToken.IsCancellationRequested) throw new TaskCanceledException(task); // Do something that takes times like a Thread.Sleep in .NET Core 2.
            Thread.Sleep(10);
            result += i;
        }

        return result;
    });

    return task;
}
在長時間運行的操做中監測  IsCancellationRequested方法 (當前是否發生取消命令),這裏我傾向去包裝一個 TaskCanceledException異常類(給上層方法調用者更多處理的可能性); 固然能夠調用 ThrowIfCancellationRequested方法拋出 OperationCanceledException異常。

② 觸發取消命令

CancellationToken結構體至關於打入在異步操做內部的楔子,隨時等候後方發來的取消命令
操縱以上CancellationToken狀態的對象是 CancellationTokenSource,這個對象是取消操做的命令發佈者。
 
默認的構造函數就支持了 超時取消:
//  如下代碼 利用 CancellationSource默認構造函數 完成超時取消
public static async Task ExecuteTaskWithTimeoutAsync(TimeSpan timeSpan)
{
    Console.WriteLine(nameof(ExecuteTaskWithTimeoutAsync));

    using (var cancellationTokenSource = new CancellationTokenSource(timeSpan))
    {
        try
        {
            var result = await LongRunningCancellableOperation(500, cancellationTokenSource.Token);
            Console.WriteLine("Result {0}", result);
        }
        catch (TaskCanceledException)
        {
            Console.WriteLine("Task was cancelled");
        }
    }
    Console.WriteLine("Press enter to continue");
    Console.ReadLine();
}

------------------------------------------------------------------------------------------------------------

 附①: 高階操做,完成手動取消:

天然咱們關注到 CancellationSource 的幾個方法, 要想在異步操做的時候 手動取消操做,須要創建另外的線程 等待手動取消操做的指令。
public static async Task ExecuteManuallyCancellableTaskAsync()
{
    Console.WriteLine(nameof(ExecuteManuallyCancellableTaskAsync));

    using (var cancellationTokenSource = new CancellationTokenSource())
    {
        // Creating a task to listen to keyboard key press
        var keyBoardTask = Task.Run(() =>
        {
            Console.WriteLine("Press enter to cancel");
            Console.ReadKey();

            // Cancel the task
 cancellationTokenSource.Cancel();
        });

        try
        {
            var longRunningTask = LongRunningCancellableOperation(500, cancellationTokenSource.Token);

            var result = await longRunningTask;
            Console.WriteLine("Result {0}", result);
            Console.WriteLine("Press enter to continue");
        }
        catch (TaskCanceledException)
        {
            Console.WriteLine("Task was cancelled");
        }

        await keyBoardTask;
    }
}
// 以上是一個控制檯程序,異步接收控制檯輸入,發出取消命令。

附②:高階操做,取消 non-Cancellable任務 :

有時候,異步操做代碼並不提供 對 Cancellation的支持,也就是以上長時間運行的異步操做
LongRunningCancellableOperation(int loop, CancellationToken cancellationToken) 並不提供參數2的傳入,至關於不容許 打入楔子。
 
這時咱們怎樣取消 這樣的non-Cancellable 任務?
 
可考慮利用 Task.WhenAny( params tasks) 操做曲線取消:
  • 利用TaskCompletionSource 註冊異步可取消任務
  • 等待待non-cancellable 操做和以上創建的 異步取消操做
private static async Task<decimal> LongRunningOperationWithCancellationTokenAsync(int loop, CancellationToken cancellationToken)
{
    // We create a TaskCompletionSource of decimal
    var taskCompletionSource = new TaskCompletionSource<decimal>();

    // Registering a lambda into the cancellationToken
    cancellationToken.Register(() =>
    {
        // We received a cancellation message, cancel the TaskCompletionSource.Task
        taskCompletionSource.TrySetCanceled();
    });

    var task = LongRunningOperation(loop);

    // Wait for the first task to finish among the two
    var completedTask = await Task.WhenAny(task, taskCompletionSource.Task);

    return await completedTask;
}

像上面代碼同樣執行取消命令 :async

public static async Task CancelANonCancellableTaskAsync()
{
    Console.WriteLine(nameof(CancelANonCancellableTaskAsync));

    using (var cancellationTokenSource = new CancellationTokenSource())
    {
        // Listening to key press to cancel
        var keyBoardTask = Task.Run(() =>
        {
            Console.WriteLine("Press enter to cancel");
            Console.ReadKey();

            // Sending the cancellation message
            cancellationTokenSource.Cancel();
        });

        try
        {
            // Running the long running task
            var longRunningTask = LongRunningOperationWithCancellationTokenAsync(100, cancellationTokenSource.Token);
            var result = await longRunningTask;

            Console.WriteLine("Result {0}", result);
            Console.WriteLine("Press enter to continue");
        }
        catch (TaskCanceledException)
        {
            Console.WriteLine("Task was cancelled");
        }

        await keyBoardTask;
    }
}

  總結:

大多數狀況下,咱們不須要編寫自定義可取消任務,由於咱們只須要使用現有API。但要知道它是如何在幕後工做老是好的。
參考資料: 
相關文章
相關標籤/搜索