首页 > 编程语言 >聊一聊 C#后台线程 如何阻塞程序退出

聊一聊 C#后台线程 如何阻塞程序退出

时间:2024-12-22 13:08:25浏览次数:5  
标签:src Thread C# work vm coreclr 聊一聊 线程

一:背景

1. 讲故事

这篇文章起源于我的 C#内功修炼训练营里的一位朋友提的问题:后台线程的内部是如何运转的 ? ,犹记得C# Via CLR这本书中 Jeffery 就聊到了他曾经给别人解决一个程序无法退出的bug,最后发现是有一个 Backgrond=false 的线程导致的。恰巧在我分析的350+dump中,也还真遇到了。有了这些铺垫,我觉得有必要简单的聊一聊。

二:后台线程的底层逻辑

1. 测试代码

为了方便讲解,先上一段代码,参考如下:


    static void Main(string[] args)
    {
        var thread = new Thread(() =>
        {
            while (true)
            {
                Console.WriteLine(DateTime.Now);
            }
        });

        thread.IsBackground = false;
        thread.Start();
    }

按照我们朴素的想法,主线程退出,程序自然就terminal,但这个程序并没有退出?原因就在于设置了 thread.IsBackground = false; 导致的,当然要想程序正常退出改为 ``thread.IsBackground = true;` 即可,接下来我们洞察下 IsBackground 有何魔力导致程序无法退出。

2. 程序为什么无法退出

要想知道这个答案,可以用 windbg 附加一下看看主线程此时正在做什么? 参考如下:


0:000> k
 # Child-SP          RetAddr               Call Site
00 0000003f`7d59e498 00007ffd`cd8d0590     ntdll!NtWaitForMultipleObjects+0x14
01 0000003f`7d59e4a0 00007ffd`8f842dd4     KERNELBASE!WaitForMultipleObjectsEx+0xf0
02 (Inline Function) --------`--------     coreclr!Thread::DoAppropriateAptStateWait+0x4a [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 3333] 
03 0000003f`7d59e790 00007ffd`8f842c25     coreclr!Thread::DoAppropriateWaitWorker+0x170 [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 3467] 
04 0000003f`7d59e850 00007ffd`8f99498e     coreclr!Thread::DoAppropriateWait+0x85 [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 3182] 
05 (Inline Function) --------`--------     coreclr!CLREventBase::WaitEx+0x26 [D:\a\_work\1\s\src\coreclr\vm\synch.cpp @ 459] 
06 (Inline Function) --------`--------     coreclr!CLREventBase::Wait+0x26 [D:\a\_work\1\s\src\coreclr\vm\synch.cpp @ 412] 
07 0000003f`7d59e8d0 00007ffd`8f94c185     coreclr!CLREventWaitWithTry+0x9a [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 5676] 
08 0000003f`7d59e980 00007ffd`8f8a062b     coreclr!ThreadStore::WaitForOtherThreads+0xabafd [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 5715] 
09 0000003f`7d59e9b0 00007ffd`8f83eaad     coreclr!RunMainPost+0x5f [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1407] 
0a 0000003f`7d59e9f0 00007ffd`8f83e0e7     coreclr!Assembly::ExecuteMainMethod+0x1f5 [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1524] 
0b 0000003f`7d59ecc0 00007ffd`8f889778     coreclr!CorHost2::ExecuteAssembly+0x267 [D:\a\_work\1\s\src\coreclr\vm\corhost.cpp @ 349] 
...

从卦中数据可以看到,主线程正在调用 ThreadStore::WaitForOtherThreads 方法,貌似是在等待其他线程完成,那具体做了什么呢?这个需要在 coreclr 上寻找答案,删减后的代码如下:


    void ThreadStore::WaitForOtherThreads()
    {
        if (!OtherThreadsComplete())
        {
            TSLockHolder.Release();

            pCurThread->SetThreadState(Thread::TS_ReportDead);

            DWORD ret = WAIT_OBJECT_0;
            while (CLREventWaitWithTry(&m_TerminationEvent, INFINITE, TRUE, &ret))
            {
            }
        }
    }

    BOOL OtherThreadsComplete()
    {
        return (m_ThreadCount - m_UnstartedThreadCount - m_DeadThreadCount
                - Thread::m_ActiveDetachCount + m_PendingThreadCount
                == m_BackgroundThreadCount);
    }

从卦中看逻辑还是非常简单的,就是因为 m_ThreadCount - m_UnstartedThreadCount - m_DeadThreadCount- Thread::m_ActiveDetachCount + m_PendingThreadCount 减完之后和 m_BackgroundThreadCount 对不上,最后在 m_TerminationEvent 事件上等待唤醒。

这里稍微提一下,这几个值可以通过 !t 显示出来,参考如下:

还有一个 Thread::m_ActiveDetachCount 计数值,这个值统计的是那种被coreclr从 ThreadStore 中移除尚未被 delete 的线程对象。结合 !t 的输出,很显然 OtherThreadsComplete()3=2 显然返回 false。因为有 1 个 background 的存在。

3. IsBackground=true 能破局吗

症结我们也找到了,只要m_TerminationEvent事件能够被唤醒,链路就会被再次打通,让程序安全退出。接下来我们研究下 IsBackground=true 在底层会做什么?简化后的C++代码如下:


    void Thread::SetBackground(BOOL isBack)
    {
        if (isBack)
        {
            if (!IsBackground())
            {
                SetThreadState(TS_Background);

                if (!IsUnstarted())
                    ThreadStore::s_pThreadStore->m_BackgroundThreadCount++;

                ThreadStore::CheckForEEShutdown();
            }
        }
    }

    void ThreadStore::CheckForEEShutdown()
    {
        if (g_fWeControlLifetime &&
            s_pThreadStore->OtherThreadsComplete())
        {
            BOOL bRet;
            bRet = s_pThreadStore->m_TerminationEvent.Set();
            _ASSERTE(bRet);
        }
    }

哈哈,卦中的化煞方法真的妙不可言,做了如下两个步骤:

  1. 做了 m_BackgroundThreadCount++,这样 OtherThreadsComplete() 的值就对上了。
  2. 使用 m_TerminationEvent.Set 做了事件唤醒,这样主线程就可以从 WaitForOtherThreads() 方法中逃出生天。

如果有些朋友没搞明白,我再画一张简图吧:

4. 判断线程的前后状态

这是最后一个要聊的话题,要想知道线程的前后状态,这个需要在 coreclr 源码中寻找答案,参考代码如下:


    void SetThreadState(ThreadState ts)
    {
        InterlockedOr((LONG*)&m_State, ts);
    }

    enum ThreadState
    {
        TS_Background = 0x00000200,    // Thread is a background thread
    }

从代码中可以看到,只要判断 ThreadState 中有没有 0x200 的标记即可,接下来用 !t 观察线程状态。


0:000> !t
ThreadCount:      4
UnstartedThread:  0
BackgroundThread: 3
PendingThread:    0
DeadThread:       0
Hosted Runtime:   no
                                                                                                            Lock  
 DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
   0    1      918 000001FA530317B0  203a220 Preemptive  000001FA574096F8:000001FA5740A5C8 000001fa530273e0 -00001 MTA 
   6    2     37c8 000001FA53009B70    21220 Preemptive  0000000000000000:0000000000000000 000001fa530273e0 -00001 Ukn (Finalizer) 
   7    3     2c7c 000001FA5307F700    2b220 Preemptive  0000000000000000:0000000000000000 000001fa530273e0 -00001 MTA 
   8    4     3bd4 0000023AE951DFD0    2b020 Preemptive  000001FA57563A08:000001FA57565010 000001fa530273e0 -00001 MTA 

从卦中可以轻松的看到 DBG=8 的线程状态是 2b020,自然就是前台线程咯。

三:总结

现在我们知道了前后台线程本质上是 coreclr 弄出来的概念,并非系统线程素有之物。还是那句话,知识不重要,重要的是会使用合适的工具和保有的探索心,这也是在训练营里重度强调的。
图片名称

标签:src,Thread,C#,work,vm,coreclr,聊一聊,线程
From: https://www.cnblogs.com/huangxincheng/p/18622015

相关文章

  • 在Cesium中从模型上采样点集数据
    在Cesium中从实景模型(如倾斜摄影模型或3DTiles模型)中采样点,通常是指获取模型表面的坐标、法线或其他属性。这可以通过以下步骤实现:方法一:使用sampleHeight函数Cesium提供了sampleHeight方法,可以从支持高度查询的地形或3DTiles数据中获取指定位置的高度。示......
  • 如何在易优CMS中使用 MyDate 函数格式化日期?
    在易优CMS中,MyDate 函数用于将时间戳格式化为指定的日期格式。这对于显示文章的发布日期、更新日期等非常有用。以下是 MyDate 函数的基本用法和一些示例:基本用法php {$eyou.field.update_time|MyDate='日期格式',###}update_time 是包含时间戳的字段。'日期格式'......
  • 如何在易优CMS中自定义ad标签中的变量名?
    在易优CMS中,如果你希望在ad标签中自定义变量名,可以使用id属性。通过设置id属性,你可以将默认的$field变量名替换为你自定义的变量名。以下是一个具体的示例:{eyou:adaid='37'id='field1'}<ahref="{$field1.links}"{$field1.target}><imgalt="{$field1.title}......
  • 如何在易优CMS中定义并使用自定义变量?
    在易优CMS中,你可以使用assign标签来定义自定义变量,并在其他标签中引用这些变量。以下是一个具体的示例:{eyou:assignname='typeid'value='5'/}{eyou:typetypeid='$typeid'}<ahref="{$field.typeurl}">{$field.typename}</a>{/eyou:type}{eyou:cha......
  • PbootCMS的模板标签有哪些特点和优势?
    PbootCMS的模板标签系统是其核心功能之一,具有高效、简洁、强大的特点,使得用户能够快速开发和维护企业网站。以下是PbootCMS模板标签的一些主要特点和优势:高效简洁:简单易学:PbootCMS的模板标签设计简单直观,即使是没有编程背景的用户也能快速上手。快速开发:通过简单的标签调用,......
  • 如何在PbootCMS中增加对WebP和MOV文件类型的上传支持?
    在PbootCMS中,默认情况下支持的上传文件类型可能不包括WebP和MOV。为了允许上传这些文件类型,你需要修改PbootCMS的配置文件和Ueditor编辑器的配置文件。以下是详细的步骤和说明:1.修改PbootCMS的配置文件PbootCMS的上传配置位于 /config/config.php 文件中。你需要在这个文件中......
  • 易优CMS中的adv标签如何使用?
    在易优CMS中,adv标签主要用于获取广告列表内容。这个标签非常灵活,可以通过设置不同的属性来控制广告的展示方式。基本的用法如下:html {eyou:advpid='1'loop='3'}<ahref="{$field.links}"><imgalt="{$field.title}"src="{$field.litpic}"/></a&g......
  • 如何在易优CMS中输出单条广告的数据?
    在易优CMS中,如果你希望输出单条广告的数据,可以使用ad标签。以下是一个具体的示例:{eyou:adaid='37'}<ahref="{$field.links}"{$field.target}><imgalt="{$field.title}"src="{$field.litpic}"/></a>{/eyou:ad}在这个......
  • dedecms教程修改网站,如何通过DedeCMS教程高效地修改网站
    DedeCMS(织梦CMS)提供了丰富的教程和文档,通过这些资源可以高效地修改网站。以下是具体步骤:访问官方文档:访问DedeCMS的官方网站,进入文档中心。这里提供了详细的用户手册、开发指南和常见问题解答。阅读教程:根据你的需求,选择相应的教程。例如,如果你想修改网站的布局,可以阅......
  • 解决 PbootCMS 附件上传报错
    根据你提供的信息,PbootCMS附件上传时报错:  上传失败:UNKNOW:Code:8192;Desc:stripos():Non-stringneedleswillbeinterpretedasstringsinthefuture.Useanexplicitchr()calltopreservethecurrentbehavior;File:/www/wwwroot/aaa.xxxx.com/core/fu......