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


背景

在.Net和C#中運行異步代碼相當簡單,因為我們有時候需要取消正在進行的異步操作,通過本文,可以掌握 通過CancellationToken取消任務(包括non-cancellable任務)。

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();
}

因為一些原因我們會取消異步操作:

  • 操作耗時較長,堵塞了其他正常請求;
  • 不願意再等待執行結果了,手動取消

編寫可取消的異步操作代碼

 

其中關注

類CancellationTokenSource:給CancellationToken發出取消通知

結構體CancellationToken: 取消操作的通知。

CancellationToken結構體相當於打入在異步操作內部的楔子,隨時等候CancellationTokenSource  發出的取消通知

 定義異步方法時候設定 CancelletionToken參數

那么這個異步方法即是Cancellable 的異步方法

/// <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狀態的對象是 CancellationTokenSource,這個對象是取消操作的命令發布者。

//  定義超時取消
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();
}

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

手動取消操作

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任務 :

有時候,部分第三方異步操作代碼並不是可取消的,也就是以上長時間運行的異步操作LongRunningCancellableOperation(int loop, CancellationToken cancellationToken) 並不支持CancellationToken ,相當於不允許打入楔子。

這時我們怎樣取消 這樣的non-Cancellable 任務?

可考慮利用 Task.WhenAny( params tasks) 操作曲線取消:

  • 利用TaskCompletionSource 注冊異步可取消任務
  • 等待待non-cancellable 操作和以上建立的 異步取消操作
private static async Task<decimal> LongRunningOperationWithCancellationTokenAsync(int loop, CancellationToken cancellationToken)
{
    // 定義一個任務完成的消息源,任務取消的動作 綁定到該任務完成的動作上
    var taskCompletionSource = new TaskCompletionSource<decimal>();
    cancellationToken.Register(() =>
    {
        taskCompletionSource.TrySetCanceled();
    });

    var task = LongRunningOperation(loop);
var completedTask = await Task.WhenAny(task, taskCompletionSource.Task); return await completedTask; }

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

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。但要知道它是如何在幕后工作總是好的。

https://johnthiriet.com/cancel-asynchronous-operation-in-csharp/

https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout

https://github.com/App-vNext/Polly/wiki/Timeout


免責聲明!

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



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