7

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

 3 years ago
source link: https://www.cnblogs.com/bluesummer/p/15219701.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

相关类型:

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,最终实现了持续监控


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK