.Net异步任务取消和监控 .Net中异步任务的取消和监控的具体实现
蓝夏 人气:0相关类型:
- CancellationTokenSource 主要用来创建或取消令牌
- CancellationToken 监听令牌状态,注册令牌取消事件
- OperationCanceledException 令牌被取消时抛出的异常,可以由监听者自主决定是否抛出异常
CancellationTokenSource
创建令牌:
CancellationTokenSource cts = new CancellationTokenSource() CancellationToken token=cts.Token;
取消释放令牌:
cts.Cancel();
CancellationToken
监听令牌取消事件:
token.Register(() => Console.WriteLine("令牌被取消"));
判断令牌是否取消
//返回一个bool,如果令牌被取消为true token.IsCancellationRequested //如果token被取消则抛出异常,内部实现其实就是判断IsCancellationRequested token.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,最终实现了持续监控
加载全部内容