代码改变世界

浅谈C#更改令牌ChangeToken

前言

    在上篇文章浅谈C#取消令牌CancellationTokenSource一文中我们讲解了CancellationTokenSource,它的主要功能就是分发一个令牌,当我取消令牌我可以进行一些回调操作或者通过令牌状态得知被取消。在上文的结尾处我们也提到了,默认情况下CancellationTokenSource产生的Token是一次性的,Cancel操作之后就没办法再复用了,只能释放掉了。而微软也很贴心的为我们提供了一个解决方案来解决这问题,那就是我们今天要说的更改令牌ChangeToken,它看起来可以让CancellationTokenSource产生的Token多次触发,本文我们就来讲解ChangeToken相关。

简单示例

    要想更好的了解一个新的知识,首先知道它是做啥的,其次要知道它怎么做。所以还是老规矩,咱们先通过简单的示例开始,这样更方便了解。ChangeToken本身是一个静态类,它的核心入口OnChange方法包含两个参数,一个是传递IChangeToken接口实例来获取令牌,另一个是令牌取消之后进行的回调操作。博主本人知道的关于IChangeToken接口的在CLR中的实现类有两个,分别是CancellationChangeTokenCompositeChangeToken类,接下来咱们就分别介绍一下这两个类的简单使用。

CancellationChangeToken示例

咱们先来演示CancellationChangeToken类的使用方式,这也是默认情况下可以使用ChangeToken的最简单方式。首先定义一个TestCancellationChangeToken类来包装一下CancellationChangeToken,实现如下

public class TestCancellationChangeToken
{
    private CancellationTokenSource tokenSource;

    /// <summary>
    /// 获取CancellationChangeToken实例方法
    /// </summary>
    public CancellationChangeToken CreatChanageToken()
    {
        tokenSource = new CancellationTokenSource();
        return new CancellationChangeToken(tokenSource.Token);
    }

    /// <summary>
    /// 取消CancellationTokenSource
    /// </summary>
    public void CancelToken()
    {
        tokenSource.Cancel();
    }
}

这个类非常简单,包含一个CancellationTokenSource类型的属性,一个创建CancellationChangeToken实例的方法和一个取消CancellationTokenSource的CancelToken方法。注意看实现的CreatChanageToken方法,这个方法每次调用都需要创建一个新的CancellationTokenSource和CancellationChangeToken实例,创建CancellationChangeToken实例需要传递CancellationToken实例。CancelToken方法里是调用的CancellationTokenSource的Cancel方法。接下来我们就来看一下如何使用定义的这个类

//声明类的实例
TestCancellationChangeToken cancellationChangeToken = new TestCancellationChangeToken();
ChangeToken.OnChange(() => cancellationChangeToken.CreatChanageToken(), () =>
{
    System.Console.WriteLine($"{DateTime.Now:HH:mm:ss}被触发可一次");
});

//模拟多次调用CancelToken
for (int i = 0; i < 3; i++)
{
    Thread.Sleep(1000);
    cancellationChangeToken.CancelToken();
}

上面的示例演示了通过ChangeToken类使用我们定义的TestCancellationChangeToken类,ChangeToken的OnChange方法传递了创建新CancellationChangeToken实例的方法委托,第二个参数则是取消令牌的回调操作,这样便可以重复的使用取消操作,为了演示效果在循环里重复调用CancelToken方法显示的打印结果是

16:40:15被触发可一次
16:40:16被触发可一次
16:40:17被触发可一次

CancellationChangeToken类是通过ChangeToken实现重复取消触发调用的简单实现,两者将结合的时候需要自己包装一下,因为ChangeToken的第一个参数需要每次获取CancellationChangeToken实例的委托,所以需要将它包装到工作类中。

CompositeChangeToken示例

实际开发中很多时候都需要一些关联的场景,比如我触发了一个取消操作,我想把和这个相关联的其它操作也取消,也就是咱们说的有相关性。CompositeChangeToken正是可以绑定一个相关性的IChangeToken集合,当这个IChangeToken集合中有任何一个实例进行取消操作的时候,当前CompositeChangeToken实例也会执行取消操作,咱们就大致演示一下它的使用方式,首先是定义一个使用类TestCompositeChangeToken来模拟包装CompositeChangeToken实例

public class TestCompositeChangeToken
{
    //声明一个CancellationTokenSource集合
    private List<CancellationTokenSource> _cancellationTokenSources;

    /// <summary>
    /// 获取CompositeChangeToken实例方法
    /// </summary>
    public CompositeChangeToken CreatChanageToken()
    {
        //初始化三个CancellationTokenSource实例
        var firstCancellationTokenSource = new CancellationTokenSource();
        var secondCancellationTokenSource = new CancellationTokenSource();
        var threeCancellationTokenSource = new CancellationTokenSource();

        //分别注册一个回调操作用于演示
        firstCancellationTokenSource.Token.Register(() => System.Console.WriteLine("firstCancellationTokenSource被取消"));
        secondCancellationTokenSource.Token.Register(() => System.Console.WriteLine("secondCancellationTokenSource被取消"));
        threeCancellationTokenSource.Token.Register(() => System.Console.WriteLine("threeCancellationTokenSource被取消"));

        //加入到集合还
        _cancellationTokenSources = new List<CancellationTokenSource>
        {
            firstCancellationTokenSource,
            secondCancellationTokenSource,
            threeCancellationTokenSource
        };

        //生成CancellationChangeToken集合
        var cancellationChangeTokens = _cancellationTokenSources.Select(i => new CancellationChangeToken(i.Token)).ToList();
        //传递给CompositeChangeToken
        var compositeChangeToken = new CompositeChangeToken(cancellationChangeTokens);
        //给CompositeChangeToken实例注册一个取消回调方便演示
        compositeChangeToken.RegisterChangeCallback(state => System.Console.WriteLine("compositeChangeToken被取消"),null);

        return compositeChangeToken;
    }

    /// <summary>
    /// 取消CancellationTokenSource
    /// </summary>
    public void CancelToken()
    {
        //方便演示效果在_cancellationTokenSources集合随便获取一个取消
        _cancellationTokenSources[new Random().Next(_cancellationTokenSources.Count)].Cancel();
    }
}

这里我定义了一个类,在获取CompositeChangeToken实例的CreatChanageToken方法中创建了三个CancellationTokenSource实例,然后用这三个实例初始化了一个CancellationChangeToken集合,用这个集合初始化了一个CompositeChangeToken实例,来模拟集合中的CancellationChangeToken实例和CompositeChangeToken实例的相关性。CancelToken方法中随机获取了一个CancellationTokenSource实例进行取消操作,来更好的演示相关性。因为CompositeChangeToken类也实现了IChangeToken接口,所以用起来都一样,大致如下

//声明类的实例
TestCompositeChangeToken compositeChangeToken = new TestCompositeChangeToken();
ChangeToken.OnChange(() => compositeChangeToken.CreatChanageToken(), () =>
{
    System.Console.WriteLine($"{DateTime.Now:HH:mm:ss}被触发可一次");
});

//模拟多次调用CancelToken
for (int i = 0; i < 3; i++)
{
    Thread.Sleep(1000);
    compositeChangeToken.CancelToken();
}

为了演示可以重复触发取消操作,这里依然使用循环的方式模拟多次触发。因为存在相关性,所以打印的执行结果如下

12:05:18被触发可一次
compositeChangeToken被取消
secondCancellationTokenSource被取消

12:05:19被触发可一次
compositeChangeToken被取消
firstCancellationTokenSource被取消

12:05:20被触发可一次
compositeChangeToken被取消
secondCancellationTokenSource被取消

从结果上可以看到任何一个相关联CancellationChangeToken实例的CancellationTokenSource实例被取消的话,与其相关的CompositeChangeToken实例也执行了取消操作,在有些场景下还是比较实用的。

源码探究

上面我们通过简单的示例大致了解了ChangeToken是做啥的,以及它怎么使用。通过名字可以得知,它叫更改令牌,说明可以动态产生令牌的值。它涉及到了几个核心的操作相关分别是IChangeToken接口、CancellationChangeToken、CompositeChangeToken和ChangeToken静态类,通过上面咱们的示例和讲解我们大致了解了这几个类型的关系,为了方便阅读和思维带入咱们就按照方便理解的顺序来挨个讲解。

友情提示:本文设计到粘贴出来的相关源码,这些源码是省略掉一部分过程的。因为我们主要是了解它的实现,无关紧要的代码可能会影响阅读效果。而且这次的GitHub源码地址我更换为https://hub.fastgit.org而没有使用官方的https://github.com,主要是GitHub近期很不稳定经常打不开。fastgit是github的镜像网站,展示的内容是完全一致的,最主要的是打开很流畅。

IChangeToken接口

首先便是IChangeToken接口,它是整个ChangeToken系列的入口操作,ChangeToken的OnChange操作也是通过它的实现类发起的。它的作用就是获取一个可以更改的令牌,也就是可以重复触发的令牌,咱们就先来看一下它的实现[点击查看源码👈]

public interface IChangeToken
{
    /// <summary>
    /// 用来标识是否发生过更改
    /// </summary>
    bool HasChanged { get; }

    /// <summary>
    /// 指示令牌是否支持回调
    /// </summary>
    bool ActiveChangeCallbacks { get; }

    /// <summary>
    /// 当令牌取消时执行的回调
    /// </summary>
    /// <param name="callback">回调执行委托</param>
    /// <param name="state">回调委托的参数</param>
    /// <returns>An <see cref="IDisposable"/> that is used to unregister the callback.</returns>
    IDisposable RegisterChangeCallback(Action<object> callback, object state);
}

它定义的接口成员非常简单,总结起来就是两类,一个是判断是否发生过更改相关即取消操作,一个是发生过更改之后的回调操作。它只是定义一个标准任何实现这个标准的类都具备被ChangeToken的OnChange方法提供可更换令牌的能力。

CancellationChangeToken实现

上面我们了解了IChageToken接口定义的成员相关,而CancellationChangeToken则是IChageToken接口最常使用默认的实现,了解了它的实现,我们就可以更好的知道ChangeToken的OnChange方法是如何工作的,所以这里我选择了先讲解CancellationChangeToken相关的实现,这样会让接下来的阅读变得更容易理解。好了直接看它的实现[点击查看源码👈]

public class CancellationChangeToken : IChangeToken
{
    /// <summary>
    /// 唯一构造函数通过CancellationChangeToken初始化
    /// </summary>
    public CancellationChangeToken(CancellationToken cancellationToken)
    {
        Token = cancellationToken;
    }

    /// <summary>
    /// 因为它是通过CancellationToken实现具备回调的能力
    /// </summary>
    public bool ActiveChangeCallbacks { get; private set; } = true;

    /// <summary>
    /// 根据CancellationToken的IsCancellationRequested属性判断令牌是否已取消
    /// </summary>
    public bool HasChanged => Token.IsCancellationRequested;

    /// <summary>
    /// 接收传递进来的CancellationToken
    /// </summary>
    private CancellationToken Token { get; }

    /// <summary>
    /// 注册回调操作
    /// </summary>
    /// <returns></returns>
    public IDisposable RegisterChangeCallback(Action<object> callback, object state)
    {
        try
        {
            //本质还是通过CancellationToken完成它回调操作的功能
            return Token.UnsafeRegister(callback, state);
        }
        catch (ObjectDisposedException)
        {
            ActiveChangeCallbacks = false;
        }
        return NullDisposable.Instance;
    }

    private class NullDisposable : IDisposable
    {
        public static readonly NullDisposable Instance = new NullDisposable();

        public void Dispose()
        {
        }
    }
}

通过上面的代码我们可以得知,CancellationChangeToken的本质还是CancellationToken的包装类,因为我们看到了CancellationChangeToken类的核心操作实现都是依赖的CancellationChangeToken类的实现完成的。它的HasChanged属性RegisterChangeCallback方法都是直接调用的CancellationChangeToken类的实现。

ChangeToken类的实现

上面我们讲解了IChangeToken接口的相关实现,也说明了因为ChangeToken类是依赖IChangeToken接口实现来完成的,所以咱们是从IChangeToken类开始讲解的。了解了上面的实现之后,咱们就可以直接来看ChangeToken相关的实现了,而我们使用的就是它的OnChange方法[点击查看源码👈]

public static IDisposable OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer)
{
    return new ChangeTokenRegistration<Action>(changeTokenProducer, callback => callback(), changeTokenConsumer);
}

public static IDisposable OnChange<TState>(Func<IChangeToken> changeTokenProducer, Action<TState> changeTokenConsumer, TState state)
{
    return new ChangeTokenRegistration<TState>(changeTokenProducer, changeTokenConsumer, state);
}

它的OnChange方法其实是包含两个重载的,一个是无参委托一个是有参委托。无参委托没啥好说的,通过有参回调我们可以给回调传递参数,这个参数是方法传递每次回调委托获取的值取决于State本身的值是什么。最重要的是它们两个都是返回了ChangeTokenRegistration<T>类的实例,也就是说OnChange方法本身是一个外观用来隐藏ChangeTokenRegistration这个类的具体信息,因为ChangeTokenRegistration只需要在ChangeToken内部使用。那我们就直接看一下ChangeTokenRegistration内部类的实现[点击查看源码👈]

private class ChangeTokenRegistration<TState> : IDisposable
{
    //生产IChangeToken实例的委托
    private readonly Func<IChangeToken> _changeTokenProducer;
    //回调委托
    private readonly Action<TState> _changeTokenConsumer;
    //回调参数
    private readonly TState _state;
    public ChangeTokenRegistration(Func<IChangeToken> changeTokenProducer, Action<TState> changeTokenConsumer, TState state)
    {
        _changeTokenProducer = changeTokenProducer;
        _changeTokenConsumer = changeTokenConsumer;
        _state = state;

        //执行changeTokenProducer得到IChangeToken实例
        IChangeToken token = changeTokenProducer();
        //调用RegisterChangeTokenCallback方法传递IChangeToken实例
        RegisterChangeTokenCallback(token);
    }
}

通过上面我们了解到ChangeTokenRegistration正是实现ChangeToken效果的核心,而它的构造函数里通过执行传递进来产生IChangeToken新实例的委托得到了新的IChangeToken实例。这里需要注意,每次执行Func<IChangeToken> 都会得到新的IChangeToken实例。然后调用了RegisterChangeTokenCallback方法,而这个方法只需要传递得到的IChangeToken实例即可。接下来我们只需要看RegisterChangeTokenCallback方法实现即可[点击查看源码👈]

private void RegisterChangeTokenCallback(IChangeToken token)
{
    //给IChangeToken实例注册回调操作
    //回调操作正是执行当前ChangeTokenRegistration实例的OnChangeTokenFired方法
    IDisposable registraton = token.RegisterChangeCallback(s => ((ChangeTokenRegistration<TState>)s).OnChangeTokenFired(), this);
    SetDisposable(registraton);
}

从这里我们可以看出ChangeTokenRegistration的RegisterChangeTokenCallback方法本质还是使用了IChangeToken实例的RegisterChangeCallback方法来实现的,不过这里的回调执行的是当前ChangeTokenRegistration实例的OnChangeTokenFired方法,也就是说令牌取消的时候调用的就是OnChangeTokenFired方法,咱们直接看一下这个方法的实现[点击查看源码👈]

private void OnChangeTokenFired()
{
    //获取一个新的IChangeToken实例
    IChangeToken token = _changeTokenProducer();
    try
    {
        //执行注册的回调操作
        _changeTokenConsumer(_state);
    }
    finally
    {
        //又调用了RegisterChangeTokenCallback注册当前IChangeToken实例
        RegisterChangeTokenCallback(token);
    }
}

看上面的代码我第一反应就是豁然开朗,通过OnChangeTokenFired方法的实现仿佛一切都透彻了。首先调用_changeTokenProducer委托获取新的IChangeToken实例,即我们通过即我们通过ChangeToken的OnChange方法第一个参数传递的委托。然后执行_changeTokenConsumer委托,即我们通过ChangeToken的OnChange方法第二个参数传递的委托。最后传递当前通过_changeTokenProducer委托产生的新IChangeToken实例调用RegisterChangeTokenCallback方法,即咱们上面的那个方法,这样就完成了类似一个递归的操作。执行完之后又将这套流程重新注册了一遍,然后形成了这种可以持续触发的操作。
上面的RegisterChangeTokenCallback方法里里调用了SetDisposable方法,这个方法主要是判断Token有没有被取消。因为我们在使用IChangeToken实例的时候会涉及到多线程共享的问题,而IChangeToken实例本身设计考虑到了线程安全问题,我们可以大致看下SetDisposable的实现[点击查看源码👈]

private IDisposable _disposable;
private static readonly NoopDisposable _disposedSentinel = new NoopDisposable();
private void SetDisposable(IDisposable disposable)
{
    //读取_disposable实例
    IDisposable current = Volatile.Read(ref _disposable);
    //如果当前_disposable实例等于_disposedSentinel实例则说明当前ChangeTokenRegistration已被释放,
    //则直接释放IChangeToken实例然后返回
    if (current == _disposedSentinel)
    {
        disposable.Dispose();
        return;
    }

    //线程安全交换,如果之前_disposable的值等于_disposedSentinel说明被释放过了
    //则释放IChangeToken实例
    IDisposable previous = Interlocked.CompareExchange(ref _disposable, disposable, current);
    if (previous == _disposedSentinel)
    {
        disposable.Dispose();
    }
    //说明没有被释放过
    else if (previous == current)
    {
    }
    //说明别的线程操作了dispose则直接异常
    else
    {
        throw new InvalidOperationException("Somebody else set the _disposable field");
    }
}

因为ChangeTokenRegistration是实现了IDisposable接口,所以我们可以先看下Dispose方法的实现,这样的话会让大家更好的理解它的这个释放体系[点击查看源码👈]

public void Dispose()
{
    //因为_disposable初始值是null所以把NoopDisposable实例赋值给_disposable并调用Dispose方法
    Interlocked.Exchange(ref _disposable, _disposedSentinel).Dispose();
}

因为初始声明_disposable变量的时候初始值是null,这里把NoopDisposable实例赋值给_disposable并调用Dispose方法。其实Dispose方法啥也没做就是为了标记一下,因为ChangeTokenRegistration类并未涉及到非托管资源相关的操作。

通过SetDisposable方法结合Dispose方法,我们可以理解在触回调操作的时候会调SetDisposable方法进行判断ChangeTokenRegistration有没有被释放过,如果已经被释放则直接释放掉传递的IChangToken实例。因为ChangeToken的OnChange方法返回的就是ChangeTokenRegistration实例,如果这个被释放则意味了OnChange传递的IChangeToken实例也必须要释放。

通过上面讲解了ChangeTokenRegistration<TState>类的实现我们了解到了ChangeToken类工作的本质,其实非常简单,为了怕大家没看明白在这里咱们简单的总结一下ChangeToken的整体工作过程。

  • ChangeToken静态类只包装了OnChange方法,这个方法传递的核心参数是产生IChangeToken实例的委托和CancellationTokenSource实例取消后的回调操作。这里的IChangeToken实例和ChangeToken静态类没啥关系,就是名字长得像。
  • ChangeToken静态类的OnChange方法本质是包装一个ChangeTokenRegistration<TState>实例。ChangeTokenRegistration是ChangeToken类工作的核心,它的工作方式是,初始化的时候生成IChangeToken实例然后调用RegisterChangeTokenCallback方法,在RegisterChangeTokenCallback方法方法中给IChangeToken实例的RegisterChangeCallback方法注册了回调操作。
  • RegisterChangeCallback回调操作注册一个调用OnChangeTokenFired方法的操作,通过上面的源码我们知道RegisterChangeCallback本质是给CancellationToken注册回调,所以当CancellationTokenSource调用Cancel的时候回执行OnChangeTokenFired方法。
  • OnChangeTokenFired方法是核心操作,它首先是获取一个新的IChangeToken实例,然后执行注册的回调操作。然后又调用了RegisterChangeTokenCallback传递了最新获取的IChangeToken实例,这样的话就形成了一个类似递归的操作,而这个递归的终止条件就是ChangeTokenRegistration有没有被释放。所以才能实现动态更改令牌的效果。

一句话总结一下就是,RegisterChangeCallback中给CancellationChangeToken的回调注册了调用OnChangeTokenFired方法的操作,OnChangeTokenFired方法中有调用了RegisterChangeCallback方法给它传递了生成的IChangeToken实例,而回调操作都是同一个,只是不断被新的IChangeToken实例调用。

CompositeChangeToken实现

上面我们说过之所以最后来说CompositeChangeToken的实现,完全是因为它属于增强的操作。如果大家理解了简单的工作方式的流程,然后再去尝试了解复杂的操作可能会更容易理解。所以咱们先说了CancellationChangeToken这个IChangeToken最简单的实现,然后说了ChangeToken静态类,让大家对整体的工作机制有了解,最后咱们再来讲解CompositeChangeToken,这样的话大家会很容易就理解这个操作方式的。咱们还是先从入口的构造函数入手吧[点击查看源码👈]

public class CompositeChangeToken : IChangeToken
{
      public IReadOnlyList<IChangeToken> ChangeTokens { get; }
      public bool ActiveChangeCallbacks { get; }
      public CompositeChangeToken(IReadOnlyList<IChangeToken> changeTokens)
      {
          ChangeTokens = changeTokens ?? throw new ArgumentNullException(nameof(changeTokens));
          //遍历传入的IChangeToken集合
          for (int i = 0; i < ChangeTokens.Count; i++)
          {
              /**
               * 如果集合中存在任何一个IChangeToken实例ActiveChangeCallbacks为true
               * 则CompositeChangeToken的ActiveChangeCallbacks也为true
               * 因为CompositeChangeToken可以关联IChangeToken集合中的任何一个有效实例
               */
              if (ChangeTokens[i].ActiveChangeCallbacks)
              {
                  ActiveChangeCallbacks = true;
                  break;
              }
          }
      }
}

从上面的构造函数可以看出IChangeToken集合中存在可用的实例即可,因为CompositeChangeToken只需要知道集合中存在可用的即可,而不是要求全部的IChangeToken都可以用。通过ChangeToken静态类的源码我们可以知道,CancellationTokenSource的Cancel方法执行后调用的是IChangeToken的RegisterChangeCallback方法,也就是说回调触发的操作就是这个方法,我们来看一下这个方法的实现[点击查看源码👈]

private CancellationTokenSource _cancellationTokenSource;
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
    //核心方法
    EnsureCallbacksInitialized();
   //这里的CancellationTokenSource注册CompositeChangeToken的回调操作
    return _cancellationTokenSource.Token.Register(callback, state);
}

private static readonly Action<object> _onChangeDelegate = OnChange;
private bool _registeredCallbackProxy;
private List<IDisposable> _disposables;
private readonly object _callbackLock = new object();
private void EnsureCallbacksInitialized()
{
   //判断是否已使用RegisterChangeCallback注册过回调操作,如果不是第一次则直接返回
    if (_registeredCallbackProxy)
    {
        return;
    }

   //加锁 意味着这个操作要线程安全
    lock (_callbackLock)
    {
        if (_registeredCallbackProxy)
        {
            return;
        }
        //实例化CancellationTokenSource,因为RegisterChangeCallback方法里再用
        _cancellationTokenSource = new CancellationTokenSource();
        _disposables = new List<IDisposable>();
        //循环要关联的IChangeToken集合
        for (int i = 0; i < ChangeTokens.Count; i++)
        {
            //判断注册进来的IChangeToken实例是否支持回调操作
            if (ChangeTokens[i].ActiveChangeCallbacks)
            {
                //给IChangeToken实例注册回调操作执行_onChangeDelegate委托
                IDisposable disposable = ChangeTokens[i].RegisterChangeCallback(_onChangeDelegate, this);
                //返回值加入IDisposable集合
                _disposables.Add(disposable);
            }
        }
       //标识注册过了,防止重复注册引发的多次触发
        _registeredCallbackProxy = true;
    }
}

上面的代码我们看到了核心的关联回调操作是执行了_onChangeDelegate委托,它是被OnChange方法初始化的。这一步操作其实就是把关联的IChangeToken实例注册_onChangeDelegate委托操作,咱们来看下CompositeChangeToken的OnChange方法实现[点击查看源码👈]

private static void OnChange(object state)
{
    //获取传递的CompositeChangeToken实例
    var compositeChangeTokenState = (CompositeChangeToken)state;
    //判断CancellationTokenSource是否被初始化过
    if (compositeChangeTokenState._cancellationTokenSource == null)
    {
        return;
    }

    //加锁 说明这一步是线程安全操作
    lock (compositeChangeTokenState._callbackLock)
    {
        try
        {
            /**
             * 取消当前实例的CancellationTokenSource
             * 这样才能执行CompositeChangeToken注册的回调操作
             */
            compositeChangeTokenState._cancellationTokenSource.Cancel();
        }
        catch
        {
        }
    }
    //获取EnsureCallbacksInitialized方法中注册的集合,即IChangeToken集合的回调返回值集合
    List<IDisposable> disposables = compositeChangeTokenState._disposables;
    //不为null则通过循环的方式挨个释放掉
    Debug.Assert(disposables != null);
    for (int i = 0; i < disposables.Count; i++)
    {
        disposables[i].Dispose();
    }
}

通过上面的OnChange方法我们得知,它主要是实现了在注册进来的任意IChangeToken实例如果发生了取消操作则当前的CompositeChangeToken实例RegisterChangeCallback进来的回调操作也要执行,而且这一步要释放掉所有注册IChangeToken实例,因为只要有一个IChangeToken实例执行了取消操作,则CompositeChangeToken实例和其它注册进来相关联的IChangeToken实例都要取消。
IChangeToken还有一个HasChange属性来标识当前IChangeToken是否被取消,咱们来看下CompositeChangeToken是如何实现这个属性的[点击查看源码👈]

public bool HasChanged
{
    get
    {
        //如果当前实例的CancellationTokenSource被取消过则说明当前CompositeChangeToken已被取消
        if (_cancellationTokenSource != null && _cancellationTokenSource.Token.IsCancellationRequested)
        {
            return true;
        }

        //循环注册进来的关联的IChangeToken集合
        for (int i = 0; i < ChangeTokens.Count; i++)
        {
            //如果存在关联的IChangeToken实例有被取消的那么也认为当前CompositeChangeToken已被取消
            if (ChangeTokens[i].HasChanged)
            {
                //调用OnChange是否关联的IChangeToken实例
                OnChange(this);
                return true;
            }
        }
        //否则则没被取消过
        return false;
    }
}

通过上面的代码可以看到HasChanged属性的设计思路符合它整体的设计思路。判断是否取消的标识有两个,如果当前实例的CancellationTokenSource被取消过则说明当前CompositeChangeToken已被取消,还有就是如果存在关联的IChangeToken实例有被取消的那么也认为当前CompositeChangeToken也被取消。好了通过上面这一部分整体的源码,我们可以总结一下CompositeChangeToken的整体实现思路。

  • CompositeChangeToken的取消回调操作分为两部分,一个是基于传递的IChangeToken集合中激活更改回调即ActiveChangeCallbacks为true的实例,另一个则是它自身维护通过RegisterChangeCallback注册进来的委托,这个委托是它内部维护的CancellationTokenSource实现的。
  • 因为CompositeChangeToken的RegisterChangeCallback方法中给注册进来的IChangeToken集合中的每一个ActiveChangeCallbacks的实例注册了取消回调操作,所以当ChangeToken静态类触发RegisterChangeCallback回调操作的时候回调用CompositeChangeToken的OnChange方法。
  • CompositeChangeToken的OnChange方法中会取消CompositeChangeToken内部维护的CancellationTokenSource,也就是触发CompositeChangeToken类本身的回调,并且释放注册进来的其他相关联的IChangeToken实例,从而实现了关联取消的操作。

    通过源码探究部分,我们分别展示了关于IChangeToken接口,以及它最简单的实现类CancellationChangeToken类的实现,然后根据CancellationChangeToken类的实现讲解了ChangeToken静态类是如何实现动态令牌更改的,最后又探究了IChangeToken接口的另一个高级的可以关联更改令牌操作的CompositeChangeToken的用法,通过这样一个流程,博主本人认为是更容易理解的。

自定义IChangeToken实现

上面我们看到了CancellationChangeToken的使用方式非常简单,但是也存在一定的限制,那就是需要外部传递CancellationTokenSource的实例。其实很多时候我们只需要知道你是IChangeToken实例就好了能满足被ChangeToken静态类使用就好了,至于传递CancellationTokenSource啥的不需要外部关心,能相应的操作就行了,比如在.Net Core的Configuration体系中的ConfigurationReloadToken,它是用来实现配置发生变化通知ConfigurationProvider重新加载数据完成自动刷新操作,我们来看一下它的实现方式[点击查看源码👈]

public class ConfigurationReloadToken : IChangeToken
{
    //内部定义了CancellationTokenSource实例
    private CancellationTokenSource _cts = new CancellationTokenSource();

    public bool ActiveChangeCallbacks => true;

    public bool HasChanged => _cts.IsCancellationRequested;

    /// <summary>
    /// 给当前的CancellationTokenSource实例注册操作
    public IDisposable RegisterChangeCallback(Action<object> callback, object state) => _cts.Token.Register(callback, state);

    /// <summary>
    /// 添加OnReload方法,供外部取消使用
    /// </summary>
    public void OnReload() => _cts.Cancel();
}

它在ConfigurationReloadToken类的内部声明了CancellationTokenSource类型的属性,然后提供了可以取消CancellationTokenSource实例的方法OnReload,这样的话逻辑可以在内部消化,而不像在外部传递。当重新获取它的实例的时候额外提供一个可获取ConfigurationReloadToken新实例的方法即可[点击查看源码👈]

private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();
private void RaiseChanged()
{
    //直接交换一个新的ConfigurationReloadToken实例
    ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
    //取消上一个ConfigurationReloadToken实例实现更改通知操作
    previousToken.OnReload();
}

这样的话获取Token的实现就非常简单了,直接返回ConfigurationReloadToken的属性即可不再需要一堆额外的操作,这样就可以保证每次通过GetReloadToken方法获取的IChangeToken实例都是未失效的。

public IChangeToken GetReloadToken() => _changeToken;

总结

    本文我们讲解了ChangeToken相关的体系,设计到了IChangeToken接口的几个实现类和ChangeToken静态类是如何实现通过取消令牌重复触发的,其实本质也就是它的名字,可以动态去更改令牌,实现的大致思路就是类似递归的操作,在回调通知里获取新的变更令牌实例,重新注册当前回调操作形成递归。因为IChangeToken实例都是引用类型,而我们传递的CancellationTokenSource实例也是引用类型,所以我们在使用的时候没感觉有什么变化,但其实如果你每次打印它的实例都是不一样的,因为内部已经更换了新的实例。好了我们大致总结一下

  • IChangeToken接口是满足ChangeToken静态类的必须操作,默认提供的CancellationChangeToken类则是IChangeToken接口最简单的实现,它是依赖CancellationTokenSource实现注册和取消通知相关操作的。
  • ChangeToken静态类的工作依赖它的OnChange方法注册的参数,一个是获取IChangeToken实例的委托,一个是令牌取消执行的操作。其实现的本质是在CancellationChangeToken的Register方法里注册重新注册的操作。也就是通过ChangeToken静态类的OnChange方法第一个参数委托,执行这个委托获取新的IChangeToken实例,当然它包含的CancellationChangeToken实例也是最新的。然后ChangeToken静态类的OnChange方法第二个参数,即回调操作重新注册给这个新的实例,这个更改操作对外部都是无感知的,但其实内部早已经更换了新的实例。
  • CompositeChangeToken实现关联取消更改操作的本质是,给一堆IChangeToken实例注册相同的OnChange操作,如果有一个IChangeToken实例执行了取消则通过OnChange方法取消当前CompositeChangeToken实例和相关联的IChangeToken实例,防止同一个CompositeChangeToken实例重复被触发。

这个系列我们讲解了取消令牌相关,其核心都是对CancellationTokenSource的包装,因为默认的CancellationTokenSource的实例默认只能被取消一次,但是很多场景需要能多次甚至无限次触发这种通知,比如.Net Core的Configuration体系,每次配置发生变更都需要执行响应的刷新操作。因此衍生出来了IChangeToken相关,结合辅助的ChangeToken来实现重复更改令牌的操作,实现无限次的触发通知。虽然博主能力和文笔都十分有限,但依然希望同学们能从中获取收获,这也是作为写作人最大的动力。

👇欢迎扫码关注我的公众号👇
posted @ 2021-09-02 09:22  yi念之间  阅读(5065)  评论(12编辑  收藏  举报