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

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

03765438bd03d86fc066f2cf2dc6b10f.gif 早期

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

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

6512c47f344c69353c39354f4e46f5ed.gif Task横空出世

Task代表一个异步操作,该类表示一个异步不返回值的操作, 泛型版本Task<TResult>表示异步有返回值的操作

可使用async/await 语法糖代码去完成异步操作。

以下创建一个简单的长时间运行的操作:

e11de56778a9da0e8e24d2ecfe3a6b75.jpeg

/// <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 模仿长时间运行的操作

 简单异步调用代码:

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

b5c124f7fb92c7eae6f2252fbc88beea.jpeg敲黑板:C#取消异步操作分为

 4db6d50efebbe6f88a9647f31e7a57d5.png

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

f208eef1a5b669b7d16c7a9658efcd78.gif  总结:

大多数情况下,我们不需要编写自定义可取消任务,因为我们只需要使用现有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_酱
出处:cnblogs.com/mi12205599/p/10572840.html

版权声明:本文来源于网络收集或网友供稿,仅供学习交流之用,如有侵权,请留言转告小编立即删除。

- EOF -

技术群:添加小编微信dotnet999

公众号:dotnet讲堂

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值