【异步编程】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。但要知道它是如何在幕后工做老是好的。
参考资料: 
相关文章
相关标签/搜索