找回密码
 立即注册
首页 业界区 业界 聊一聊 .NET 中的 CancellationTokenSource

聊一聊 .NET 中的 CancellationTokenSource

奄幂牛 5 小时前
一:背景

1. 讲故事

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

1. 一个简单的案例

在.NET SDK框架代码中有大量的 CancellationTokenSource 应用,也是被遗弃的Thread.Abort的替代品,为了方便讲述,先写一段简单的代码,通过CancelAfter 让执行流在 2s 后实现中断,参考代码如下:
  1.     static void Main()
  2.     {
  3.         var cts = new CancellationTokenSource();
  4.         // 注册取消回调
  5.         cts.Token.Register(() => { Console.WriteLine("1. 取消回调被执行..."); });
  6.         cts.Token.Register(() => Console.WriteLine("2. 取消回调被执行..."));
  7.         cts.CancelAfter(2000); // 2秒后自动取消
  8.         Console.WriteLine("任务开始,2秒后自动取消...");
  9.         try
  10.         {
  11.             for (int i = 0; i < 10; i++)
  12.             {
  13.                 cts.Token.ThrowIfCancellationRequested();
  14.                 Console.WriteLine($"处理 {i}");
  15.                 Thread.Sleep(500);
  16.             }
  17.         }
  18.         catch (OperationCanceledException)
  19.         {
  20.             Console.WriteLine("任务被取消!");
  21.         }
  22.         Console.ReadKey();
  23.     }
复制代码
1.png

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

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

根据地图描述,每一个 Register 函数都被封装成一个 CallbackNode 节点,并最终构建出一个 双向链表,这个链表的头节点会记录到 Registrations.Callbacks 字段上,简化后的代码如下:
  1. internal CancellationTokenRegistration Register(Delegate callback, object stateForCallback, SynchronizationContext syncContext, ExecutionContext executionContext)
  2. {
  3.     if (!this.IsCancellationRequested)
  4.     {
  5.         long id = 0L;
  6.         if (callbackNode == null)
  7.         {
  8.             callbackNode = new CancellationTokenSource.CallbackNode(registrations);
  9.             callbackNode.Callback = callback;
  10.             callbackNode.CallbackState = stateForCallback;
  11.             callbackNode.ExecutionContext = executionContext;
  12.             callbackNode.SynchronizationContext = syncContext;
  13.             registrations.EnterLock();
  14.             try
  15.             {
  16.                 CancellationTokenSource.CallbackNode callbackNode3 = callbackNode;
  17.                 CancellationTokenSource.Registrations registrations3 = registrations;
  18.                 long nextAvailableId = registrations3.NextAvailableId;
  19.                 registrations3.NextAvailableId = nextAvailableId + 1L;
  20.                 id = (callbackNode3.Id = nextAvailableId);
  21.                 callbackNode.Next = registrations.Callbacks;
  22.                 if (callbackNode.Next != null)
  23.                 {
  24.                     callbackNode.Next.Prev = callbackNode;
  25.                 }
  26.                 registrations.Callbacks = callbackNode;
  27.             }
  28.             finally
  29.             {
  30.                 registrations.ExitLock();
  31.             }
  32.         }
  33. }
复制代码
接下来就是如何眼见为实?可以使用 dnspy 来调试,在 registrations.ExitLock(); 处下一个断点,截图如下:
3.png

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

  • 从 callbackNode.id来看,这个链表采用头插法,即注册的Register是后进先出。
  • CallbackState 存放着我们自定义的回调。
  • NextAvailableId 记录着接下来需要分配的 callbackNode.id 。
链表构建好之后,接下来就是如何调用了。
3. cts.CancelAfter 底层发生了什么

可以使用 dnspy 调试源代码,观察下如何实现 2s 后自动触发取消操作,简化后核心代码如下:
  1. private void CancelAfter(uint millisecondsDelay)
  2. {
  3.     ITimer timer = this._timer;
  4.     if (timer == null)
  5.     {
  6.         timer = new TimerQueueTimer(CancellationTokenSource.s_timerCallback, this, uint.MaxValue, uint.MaxValue, false);
  7.     }
  8.     timer.Change((millisecondsDelay == uint.MaxValue) ? Timeout.InfiniteTimeSpan : TimeSpan.FromMilliseconds(millisecondsDelay), Timeout.InfiniteTimeSpan);
  9. }
  10. private static readonly TimerCallback s_timerCallback = delegate (object obj)
  11. {
  12.     ((CancellationTokenSource)obj).NotifyCancellation(throwOnFirstException: false);
  13. };
复制代码
从卦中可以看到,所谓的 CancelAfter(2000) 是用Timer定时器来实现的,时间一到自会执行 s_timerCallback 回调函数。
接下来继续研究下内部的 NotifyCancellation 方法,根据前面的分析应该就是把 Registrations.Callbacks 中的节点全部提取出来,简化后的核心代码如下:
  1. private void ExecuteCallbackHandlers(bool throwOnFirstException)
  2. {
  3.     registrations.ThreadIDExecutingCallbacks = Environment.CurrentManagedThreadId;
  4.     for (; ; )
  5.     {
  6.         registrations.EnterLock();
  7.         CancellationTokenSource.CallbackNode callbacks;
  8.         try
  9.         {
  10.             callbacks = registrations.Callbacks;
  11.             if (callbacks == null)
  12.             {
  13.                 break;
  14.             }
  15.             if (callbacks.Next != null)
  16.             {
  17.                 callbacks.Next.Prev = null;
  18.             }
  19.             registrations.Callbacks = callbacks.Next;
  20.             registrations.ExecutingCallbackId = callbacks.Id;
  21.             callbacks.Id = 0L;
  22.         }
  23.         finally
  24.         {
  25.             registrations.ExitLock();
  26.         }
  27.         callbacks.ExecuteCallback();
  28.     }
  29. }
  30. public void ExecuteCallback()
  31. {
  32.     ExecutionContext.RunInternal(executionContext, delegate (object s)
  33.     {
  34.         CancellationTokenSource.CallbackNode callbackNode = (CancellationTokenSource.CallbackNode)s;
  35.         CancellationTokenSource.Invoke(callbackNode.Callback, callbackNode.CallbackState, callbackNode.Registrations.Source);
  36.     }, this);
  37. }
复制代码
从卦中代码可以提取到如下几点信息。

  • ThreadIDExecutingCallbacks 这是一个很好的统计字段,记录着当前谁正在执行 Cancel 方法。
  • ExecutingCallbackId  同样一个很好的统计字段,记录着从链表 registrations.Callbacks 中已提取出来的 Node 信息。
  • for 循环一次性的提取 registrations.Callbacks 中的所有节点。
最后我们用 dnspy 在 callbacks.ExecuteCallback() 函数末尾处下一个断点,截图如下:
4.png

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

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

来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除

相关推荐

您需要登录后才可以回帖 登录 | 立即注册