聊一聊 .NET 中的 CompositeChangeToken
一:背景
1. 講故事
上一篇跟大家聊到了 CancellationTokenSource,今天跟大家聊到的是另一個話題叫組合變更令牌 CompositeChangeToken,當前我所有的研究都是基于dump分析之用,所以偏重的點自然就不一樣,如果純純的研究源碼那可能就是入門到放棄。。。接下來說下 CompositeChangeToken是干什么用的,你可以理解成觀察者模式,舉例:如果一個房子里面有幾顆炸彈,只要任何一顆炸彈爆炸,房子都會塌掉,任何關注這個房子的人都會有所變化(跑,叫,哭)... ,其中 CompositeChangeToken 就是觀察者集合,有了這個概念之后寫一段簡單的代碼。
namespace BombHouseExample
{
internal class Program
{
static void Main(string[] args)
{
// 創建多個炸彈(變化令牌)
var bomb1 = new BombChangeToken("炸彈1");
var bomb2 = new BombChangeToken("炸彈2");
var bomb3 = new BombChangeToken("炸彈3");
// 創建組合令牌 - 任何炸彈爆炸都會觸發房子倒塌
var houseToken = new CompositeChangeToken(new IChangeToken[] { bomb1, bomb2, bomb3 });
Console.WriteLine("房子里有幾顆炸彈,任何一顆爆炸都會導致房子倒塌!");
Console.WriteLine("觀察者(回調)已注冊:當房子倒塌時會有不同反應...\n");
// 注冊不同的觀察者反應
houseToken.RegisterChangeCallback(_ =>
{
Console.WriteLine($"【{DateTime.Now:HH:mm:ss}】 觀察者1: 驚聲尖叫!");
}, null);
houseToken.RegisterChangeCallback(_ =>
{
Console.WriteLine($"【{DateTime.Now:HH:mm:ss}】 觀察者2: 拼命逃跑!");
}, null);
houseToken.RegisterChangeCallback(_ =>
{
Console.WriteLine($"【{DateTime.Now:HH:mm:ss}】 觀察者3: 放聲大哭!");
}, null);
// 模擬炸彈爆炸
Console.WriteLine("\n3秒后炸彈爆炸...");
Thread.Sleep(3000);
bomb1.Explode(); // 任何一顆炸彈爆炸都會觸發所有回調
Console.WriteLine("\n按任意鍵退出...");
Console.ReadKey();
}
}
/// <summary>
/// 炸彈變化令牌 - 任何炸彈爆炸都會導致房子倒塌
/// </summary>
public class BombChangeToken : IChangeToken
{
private CancellationTokenSource _cts = new CancellationTokenSource();
private string _bombName;
public BombChangeToken(string bombName)
{
_bombName = bombName;
}
/// <summary>
/// 引爆炸彈
/// </summary>
public void Explode()
{
Console.WriteLine($"【{_bombName}】爆炸了!");
_cts.Cancel();
}
public bool HasChanged => _cts.IsCancellationRequested;
public bool ActiveChangeCallbacks => true;
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
return _cts.Token.Register(callback, state);
}
}
}

從卦中看,是不是非常的形象,當然這不是本篇的主題,接下來簡單研究下底層。
二:CompositeChangeToken 分析
1. RegisterChangeCallback 干了什么
這個方法在底層會做兩件事情。
- 在
BombChangeToken中注入CompositeChangeToken.OnChange方法 - 在
CompositeChangeToken中注入用戶自定義回調。
千言萬語之前先來一張類圖,我花了點時間自己研究的,不知道對不對哈。

手握地圖接下來就是如何眼見為實呢?先觀察源代碼。
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
this.EnsureCallbacksInitialized();
return this._cancellationTokenSource.Token.Register(callback, state); //第二件事
}
private void EnsureCallbacksInitialized()
{
if (!this.RegisteredCallbackProxy)
{
this._cancellationTokenSource = new CancellationTokenSource();
this._disposables = new List<IDisposable>();
for (int i = 0; i < this.ChangeTokens.Count; i++)
{
if (this.ChangeTokens[i].ActiveChangeCallbacks)
{
IDisposable disposable = this.ChangeTokens[i].RegisterChangeCallback(CompositeChangeToken._onChangeDelegate, this); //第一件事
if (this._cancellationTokenSource.IsCancellationRequested)
{
disposable.Dispose();
break;
}
this._disposables.Add(disposable);
}
}
}
}
有了源代碼的陪伴,接下來使用 dnspy 在 bomb.Explode() 處下斷點觀察,截圖如下:

從卦中可以看到 BombChangeToken 注冊的果然是 OnChange,眼尖的朋友會看到這里有一個 CallbackPartition[16], 稍微解釋下,它是為了提高并發,將原來的 Registrations 拆分成了 16 個,相當于有 16 個 CallbackNode 分配器。
接下來看另一張圖:

從卦中可以看到這里也有一個自己的 cts,從 Id=3 可以知道里面有 3-1 個CallbackNode,即我們注冊的自定義回調。
這里有一個小注意點,多個 BombChangeToken 和 單個 CompositeChangeToken 內部都有自己的 cts,這個在研究的時候不要以為是一個,搞得暈頭轉向的。
2. Explode 是如何觸發的
想了解這個觸發過程,畫一張序列圖如下:

從卦中可以清晰的看到 BombChangeToken.TriggerChange() -> CompositeChangeToken.OnChange() -> CancellationTokenRegistration.Dispose() 的過程,接下來就是眼見為實環節了,在 CompositeChangeToken.OnChange 處下一個斷點觀察。

最后就是要明白 Composite._cancellationTokenSource 中的一些重要調試信息,比如:
- _threadIDExecutingCallbacks 當前執行取消操作的線程ID。
- _executingCallbackId 當前正在處理哪一個CallbackNode 節點。
上面的兩點信息在高級調試排查中非常重要,截圖如下:
三:總結
到這里就簡單的介紹完了,重點留意 _threadIDExecutingCallbacks 和 _executingCallbackId 字段值是解決程序中疑難雜癥的關鍵。

浙公網安備 33010602011771號