淺談C#更改令牌ChangeToken

yi念之間發表於2021-09-02

前言

    在上篇文章淺談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來實現重複更改令牌的操作,實現無限次的觸發通知。雖然博主能力和文筆都十分有限,但依然希望同學們能從中獲取收穫,這也是作為寫作人最大的動力。

?歡迎掃碼關注我的公眾號? 淺談C#更改令牌ChangeToken