【你不一定知晓的】C#取消异步操作

在.Net和C#中运行异步代码相当简单,因为我们有时候需要取消正在进行的异步操作,通过本文,可以掌握 通过CancellationToken取消任务(包括non-cancellable任务)。

 早期

早期.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

 已经不再推荐这种方式来完成异步和长时间运行的操作,但是大部分概念在现在依旧可以使用。

 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#取消异步操作分为2步

① 让代码可取消(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;
}

像上面代码一样执行取消命令 :

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   作者:
Julian_酱

感谢您的认真阅读,如有问题请大胆斧正,如果您觉得本文对你有用,不妨右下角点个或加关注。

本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置注明本文的作者及原文链接,否则保留追究法律责任的权利。

 

人已赞赏
随笔日记

看过太多大厂面试题,其实考的无非是这 3 点能力

2020-11-9 3:56:02

随笔日记

RabbitMQ消息队列

2020-11-9 3:56:04

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索