• 【异步编程】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

  • 相关阅读:
    作为计算机专业的过来人:想学好C 语言,你必须要知道这3个原因(强烈推荐)
    C语言从1972年诞生至今,能够恒久不衰,是怎么做到的?
    这4种胜过C语言的 C++ 强制转换方法,真的好用到爆...(建议收藏)
    一纸学历证书真的胜过多年工作经验吗?HR更看重程序员什么地方?
    《个人-GIT使用方法》
    java Comparable 和 Cloneable接口
    Java 文本I/O 处理
    java 异常处理
    java的一维数组
    java中String类型
  • 原文地址:https://www.cnblogs.com/JulianHuang/p/10572840.html
Copyright © 2020-2023  润新知