C# 中异步任务Task的取消和监控

在.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    }}
 

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

图片

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

 简单异步调用代码:

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

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

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

.Net中异步任务的取消和监控

相关类型:

CancellationTokenSource 主要用来创建或取消令牌

CancellationToken 监听令牌状态,注册令牌取消事件

OperationCanceledException 令牌被取消时抛出的异常,可以由监听者自主决定是否抛出异常

CancellationTokenSource

创建令牌:

 
CancellationTokenSource cts = new CancellationTokenSource()
CancellationToken token=cts.Token;

取消释放令牌:

cts.Cancel();


 

CancellationToken

监听令牌取消事件:

token.Register(() => Console.WriteLine("令牌被取消"));

判断令牌是否取消

 
//返回一个bool,如果令牌被取消为truetoken.IsCancellationRequested
//如果token被取消则抛出异常,内部实现其实就是判断IsCancellationRequestedtoken.ThrowIfCancellationRequested()=>{  if(token.IsCancellationRequested){    throw new OperationCanceledException();  }}


 

代码示例

下面模拟一个文件下载的任务,在未下载完成后下载任务被取消

 
 public void Run() {     CancellationTokenSource cts = new CancellationTokenSource();
     Task.Run(() =>              {                  //等待两秒后取消,模拟的是用户主动取消下载任务                  Thread.Sleep(2000);                  cts.Cancel();              });
     try     {         var size = DownloadFile(cts.Token);         Console.WriteLine("文件大小:" + size);     }     catch (OperationCanceledException)     {         Console.WriteLine("下载失败");     }finally{         cts.Dispose();     }     Thread.Sleep(2000); }

/// <summary>/// 模拟下载文件,下载文件需要五秒/// </summary>/// <returns></returns>public int DownloadFile(CancellationToken token){    token.Register(() =>                   {                       System.Console.WriteLine("监听到取消事件");                   });
    Console.WriteLine("开始下载文件");    for (int i = 0; i < 5; i++)    {        token.ThrowIfCancellationRequested();        Console.WriteLine(i.ToString());        Thread.Sleep(1000);    }    Console.WriteLine("文件下载完成");    return 100;}

输出结果:

开始下载文件
0
1
监听到取消事件
下载失败

思考

为什么要将CancellationToken和CancellationTokenSource分为两个类呢,直接一个CancellationToken又可以取消又可以判断状态注册啥的不是更好,更方便?

其实每种类的设计和实现都可以有很多不同的策略,CTS和CT从这个两个类提供的为数不多的公开方法中就可以看出,CTS用来控制Token的生成和取消等生命周期状态,CT只能用来监听和判断,无法对Token的状态进行改变。

所以这种设计的目的就是关注点分离。限制了CT的功能,避免Token在传递过程中被不可控的因素取消造成混乱。


 

关联令牌

继续拿上面的示例来说,示例中实现了从外部控制文件下载功能的终止。

如果要给文件下载功能加一个超时时间的限制,此时可以增加一个控制超时时间的token,将外部传来的token和内部token 关联起来变为一个token

只需要将DownloadFile()函数做如下改造即可

 
public int DownloadFile(CancellationToken externalToken)        {            //通过构造函数设置TokenSource一秒之后调用Cancel()函数            var timeOutToken = new CancellationTokenSource(new TimeSpan(0, 0, 1)).Token;            using (var linkToken = CancellationTokenSource.CreateLinkedTokenSource(externalToken, timeOutToken))            {                Console.WriteLine("开始下载文件");                for (int i = 0; i < 5; i++)                {                    linkToken.Token.ThrowIfCancellationRequested();                    Console.WriteLine(i.ToString());                    Thread.Sleep(1000);                }                Console.WriteLine("文件下载完成");                return 100;            }        }

此时不论是externalToken取消,或是timeOutToken取消,都会触发linkToken的取消事件


 

CancellationChangeToken

CancellationChangeToken主要用来监测目标变化,需配合ChangeToken使用。从功能场景来说,其实ChangeToken的功能和事件似乎差不多,当监控的目标发生了变化,监听者去做一系列的事情。

但是事件的话,监听者需要知道目标的存在,就是如果A要注册B的事件,A是要依赖B的。

CancellationChangeToken是基于CancellationToken来实现的,可以做到依赖于Token而不直接依赖被监听的类

创建CancellationChangeToken:

new CancellationChangeToken(new CancellationTokenSource().Token)

监听Token变动

new CancellationChangeToken(cts.Token).RegisterChangeCallback(obj => Console.WriteLine("token 变动"), null);

CancellationChangeToken只是把CancellationToken包装了一层。RegisterChangeCallback最终也是监听的CancellationToken的IsCancellationRequested状态。

所以就有个问题,代码写到这里,并不能实现每次内部变动都触发回调事件。

因为CT只会Cancel一次,对应的监听也会执行一次。无法实现多次监听

为了实现变化的持续监听,需要做两个操作

  • 让Token在Cancel之后重新初始化

  • 每次Cancel回调之后重新监听新的Token

先上代码,下面的代码实现了每次时间变动都会通知展示面板刷新时间的显示

 
public void Run(){    var bjDate = new BeijingDate();    DisplayDate(bjDate.GetChangeToken, bjDate.GetDate);    Thread.Sleep(50000);}
public void DisplayDate(Func<IChangeToken> getChangeToken, Func<DateTime> getDate){    ChangeToken.OnChange(getChangeToken, () => Console.WriteLine("当前时间:" + getDate()));}
public class BeijingDate{    private CancellationTokenSource cts;    private DateTime date;    public BeijingDate()    {        cts = new CancellationTokenSource();        var timer = new Timer(TimeChange, null, 0, 1000);    }
    private void TimeChange(object state)    {        date = DateTime.Now;        var old = cts;        cts = new CancellationTokenSource();        old.Cancel();    }
    public DateTime GetDate() => date;    public CancellationChangeToken GetChangeToken()    {        return new CancellationChangeToken(cts.Token);    }}

TimeChange()中修改了时间,重置了Token并将旧的Token取消

DisplayDate中用ChangeToken.OnChange获取对应的Token并监听

实现了DisplayData函数和BeijingDate这个类的解耦

ChangeToken.OnChange 这个函数接收两个参数,一个是获取Token的委托,一个是Token取消事件的响应委托。

每次在处理完Token的取消事件后,他会重新调用第一个委托获取Token,而此时我们已经生成了新的Token,最终实现了持续监控

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值