聊一聊 .NET 中的 CancellationTokenSource

聊一聊 .NET 中的 CancellationTokenSource

    正在检查是否收录...

一:背景

1. 讲故事

.NET高级调试中,我们需要知道很多的C#底层细节,如果搞不清这些底层细节,那与之相关的故障可能就搞不定,所以调试这个东西需要我们有一个比较广的知识面,痛苦哈,比如这篇跟大家聊到的 CancellationTokenSource 。

二:CancellationTokenSource 分析

1. 一个简单的案例

.NET SDK框架代码中有大量的 CancellationTokenSource 应用,也是被遗弃的Thread.Abort的替代品,为了方便讲述,先写一段简单的代码,通过CancelAfter 让执行流在 2s 后实现中断,参考代码如下:

 static void Main() { var cts = new CancellationTokenSource(); // 注册取消回调 cts.Token.Register(() => { Console.WriteLine("1. 取消回调被执行..."); }); cts.Token.Register(() => Console.WriteLine("2. 取消回调被执行...")); cts.CancelAfter(2000); // 2秒后自动取消 Console.WriteLine("任务开始,2秒后自动取消..."); try { for (int i = 0; i < 10; i++) { cts.Token.ThrowIfCancellationRequested(); Console.WriteLine($"处理 {i}"); Thread.Sleep(500); } } catch (OperationCanceledException) { Console.WriteLine("任务被取消!"); } Console.ReadKey(); } 

聊一聊 .NET 中的 CancellationTokenSource

代码看起来好像是这么一回事,但很少人知道 Register,CancelAfter 底层到底都发生了什么?这也是本篇需要探索的东西,为了能够让大家手握地图,我花了点时间看了下代码画了如下的架构图,截图如下:

聊一聊 .NET 中的 CancellationTokenSource

2. Token.Register 底层发生了什么

根据地图描述,每一个 Register 函数都被封装成一个 CallbackNode 节点,并最终构建出一个 双向链表,这个链表的头节点会记录到 Registrations.Callbacks 字段上,简化后的代码如下:

 internal CancellationTokenRegistration Register(Delegate callback, object stateForCallback, SynchronizationContext syncContext, ExecutionContext executionContext) { if (!this.IsCancellationRequested) { long id = 0L; if (callbackNode == null) { callbackNode = new CancellationTokenSource.CallbackNode(registrations); callbackNode.Callback = callback; callbackNode.CallbackState = stateForCallback; callbackNode.ExecutionContext = executionContext; callbackNode.SynchronizationContext = syncContext; registrations.EnterLock(); try { CancellationTokenSource.CallbackNode callbackNode3 = callbackNode; CancellationTokenSource.Registrations registrations3 = registrations; long nextAvailableId = registrations3.NextAvailableId; registrations3.NextAvailableId = nextAvailableId + 1L; id = (callbackNode3.Id = nextAvailableId); callbackNode.Next = registrations.Callbacks; if (callbackNode.Next != null) { callbackNode.Next.Prev = callbackNode; } registrations.Callbacks = callbackNode; } finally { registrations.ExitLock(); } } } 

接下来就是如何眼见为实?可以使用 dnspy 来调试,在 registrations.ExitLock(); 处下一个断点,截图如下:

聊一聊 .NET 中的 CancellationTokenSource

从卦中可以看到如下信息:

  1. callbackNode.id来看,这个链表采用头插法,即注册的Register是后进先出
  2. CallbackState 存放着我们自定义的回调。
  3. NextAvailableId 记录着接下来需要分配的 callbackNode.id

链表构建好之后,接下来就是如何调用了。

3. cts.CancelAfter 底层发生了什么

可以使用 dnspy 调试源代码,观察下如何实现 2s 后自动触发取消操作,简化后核心代码如下:

 private void CancelAfter(uint millisecondsDelay) { ITimer timer = this._timer; if (timer == null) { timer = new TimerQueueTimer(CancellationTokenSource.s_timerCallback, this, uint.MaxValue, uint.MaxValue, false); } timer.Change((millisecondsDelay == uint.MaxValue) ? Timeout.InfiniteTimeSpan : TimeSpan.FromMilliseconds(millisecondsDelay), Timeout.InfiniteTimeSpan); } private static readonly TimerCallback s_timerCallback = delegate (object obj) { ((CancellationTokenSource)obj).NotifyCancellation(throwOnFirstException: false); }; 

从卦中可以看到,所谓的 CancelAfter(2000) 是用Timer定时器来实现的,时间一到自会执行 s_timerCallback 回调函数。

接下来继续研究下内部的 NotifyCancellation 方法,根据前面的分析应该就是把 Registrations.Callbacks 中的节点全部提取出来,简化后的核心代码如下:

 private void ExecuteCallbackHandlers(bool throwOnFirstException) { registrations.ThreadIDExecutingCallbacks = Environment.CurrentManagedThreadId; for (; ; ) { registrations.EnterLock(); CancellationTokenSource.CallbackNode callbacks; try { callbacks = registrations.Callbacks; if (callbacks == null) { break; } if (callbacks.Next != null) { callbacks.Next.Prev = null; } registrations.Callbacks = callbacks.Next; registrations.ExecutingCallbackId = callbacks.Id; callbacks.Id = 0L; } finally { registrations.ExitLock(); } callbacks.ExecuteCallback(); } } public void ExecuteCallback() { ExecutionContext.RunInternal(executionContext, delegate (object s) { CancellationTokenSource.CallbackNode callbackNode = (CancellationTokenSource.CallbackNode)s; CancellationTokenSource.Invoke(callbackNode.Callback, callbackNode.CallbackState, callbackNode.Registrations.Source); }, this); } 

从卦中代码可以提取到如下几点信息。

  1. ThreadIDExecutingCallbacks 这是一个很好的统计字段,记录着当前谁正在执行 Cancel 方法。
  2. ExecutingCallbackId 同样一个很好的统计字段,记录着从链表 registrations.Callbacks 中已提取出来的 Node 信息。
  3. for 循环一次性的提取 registrations.Callbacks 中的所有节点。

最后我们用 dnspy 在 callbacks.ExecuteCallback() 函数末尾处下一个断点,截图如下:

聊一聊 .NET 中的 CancellationTokenSource

从卦中可以看到,Callbacks已被清空,最后一个函数节点是 CallbackNode.id=1 ,并且执行这个 Cancel() 方法的线程是5号线程。

三:总结

如今越来越多的底层方法加上了 CancellationTokenSource 取消机制以及 CompositeChangeToken,一旦开发者使用不当导致底层产生了卡死,死锁等一系列问题时,对我们调试者来说真的是亚历山大。

聊一聊 .NET 中的 CancellationTokenSource
  • 本文作者:WAP站长网
  • 本文链接: https://wapzz.net/post-27854.html
  • 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。
本站部分内容来源于网络转载,仅供学习交流使用。如涉及版权问题,请及时联系我们,我们将第一时间处理。
文章很赞!支持一下吧 还没有人为TA充电
为TA充电
还没有人为TA充电
0
0
  • 支付宝打赏
    支付宝扫一扫
  • 微信打赏
    微信扫一扫
感谢支持
文章很赞!支持一下吧
关于作者
2.8W+
9
1
2
WAP站长官方

淘宝:将公益拍卖102套马云签名骑士制服

上一篇

你的ChatBI(问数)准确率不到50%?带你深度拆解90%准确率的高德ChatBI案例

下一篇
评论区
内容为空

这一切,似未曾拥有

  • 复制图片
按住ctrl可打开默认菜单