惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Attack and Defense Labs
Attack and Defense Labs
aimingoo的专栏
aimingoo的专栏
月光博客
月光博客
D
Darknet – Hacking Tools, Hacker News & Cyber Security
K
Kaspersky official blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Stack Overflow Blog
Stack Overflow Blog
T
Threatpost
AWS News Blog
AWS News Blog
小众软件
小众软件
美团技术团队
L
Lohrmann on Cybersecurity
Hacker News: Ask HN
Hacker News: Ask HN
Security Latest
Security Latest
Jina AI
Jina AI
Hacker News - Newest:
Hacker News - Newest: "LLM"
宝玉的分享
宝玉的分享
A
Arctic Wolf
M
MIT News - Artificial intelligence
Scott Helme
Scott Helme
爱范儿
爱范儿
The Cloudflare Blog
博客园 - 聂微东
云风的 BLOG
云风的 BLOG
博客园 - 叶小钗
有赞技术团队
有赞技术团队
The GitHub Blog
The GitHub Blog
T
The Blog of Author Tim Ferriss
I
InfoQ
V
Visual Studio Blog
N
Netflix TechBlog - Medium
Latest news
Latest news
C
CERT Recently Published Vulnerability Notes
T
Tenable Blog
H
Heimdal Security Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Cisco Talos Blog
Cisco Talos Blog
L
LINUX DO - 最新话题
博客园 - 三生石上(FineUI控件)
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Threat Research - Cisco Blogs
S
Secure Thoughts
The Hacker News
The Hacker News
S
Schneier on Security
博客园 - 【当耐特】
T
The Exploit Database - CXSecurity.com
博客园_首页
N
News | PayPal Newsroom
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org

博客园 - 一线码农

记一次 .NET 某智慧医保云服务Linux 非托管泄露分析 .NET 高级调试技术:超越基础 Dump 分析 .NET 生产环境调试实战指南 记一次 .NET 某光电成像后端解析系统 内存暴涨分析 记一次 .NET 某电力后台监控系统 内存暴涨分析 记一次 .NET 某注塑模具系统 CPU爆高分析 记一次 .NET 某集群管理软件 内存暴涨分析 记一次 .NET 某智慧工厂视觉程序 崩溃分析 记一次 .NET 某低代码开发框架 内存暴涨分析 记一次 .NET 某MES上位机拍照系统 内存暴涨分析 记一次 .NET 某RFID标签打印客户端 崩溃分析 对 .NET FileSystemWatcher引发内存碎片化的 反思 DotMemory系列:5. 如何实现自动化抓取和应用自托管 DotMemory系列:4. 如何分析进程的转储文件 DotMemory系列:3. 堆碎片化引发的内存暴涨分析 DotMemory系列:2. 事件泄露引发的内存暴涨分析 DotMemory系列:1. 终结队列积压引发的内存暴涨分析 记一次 .NET 某理财管理客户端 OOM溢出分析 记一次 .NET 某医联体管理系统 崩溃分析 记一次 .NET 某光放测试系统 崩溃分析 记一次 .NET 某药品缺陷高速检测系统 卡慢分析 聊一聊 .NET超高内存故障分析方法 的反思 记一次 .NET 某企业ECM内容管理系统 内存暴涨分析 记一次 .NET 某跨境物流系统 内存暴涨分析 记一次 .NET 某中医药附属医院门诊系统 崩溃分析 聊一聊 .NET 中的 CompositeChangeToken 记一次 .NET 某CRM物流行业管理系统 崩溃分析
聊一聊 .NET 中的 CancellationTokenSource
一线码农 · 2025-09-05 · via 博客园 - 一线码农

一:背景

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();
    }

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

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(); 处下一个断点,截图如下:

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

  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() 函数末尾处下一个断点,截图如下:

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

三:总结

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

图片名称