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

一:背景

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 弄出来的概念,并非系统线程素有之物。还是那句话,知识不重要,重要的是会使用合适的工具和保有的探索心,这也是在训练营里重度强调的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/943260.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

带着国标充电器出国怎么办? 适配器模式(Adapter Pattern)

适配器模式(Adapter Pattern) 适配器模式适配器模式(Adapter Pattern)概述talk is cheap, show you my code总结 适配器模式 适配器模式(Adapter Pattern)是面向对象软件设计中的一种结构型设计…

【技术实战】R语言统计分析与可视化从入门到精通

前言 随着大数据时代的到来,数据分析已经成为各行各业的重要技能。R语言作为一种强大的统计分析和数据可视化工具,广泛应用于科学研究、数据分析和商业决策支持。 本文将带领读者从入门到精通,掌握R语言在统计分析和数据可视化方面的核心技…

Milvus×EasyAi:如何用java从零搭建人脸识别应用

如何从零搭建一个人脸识别应用?不妨试试原生Java人工智能算法:EasyAi Milvus 的组合拳。 本文将使用到的软件和工具包括: EasyAi:人脸特征向量提取Milvus:向量数据库用于高效存储和检索数据。 01. EasyAi:…

【运维】文件存储公共盘升级及数据迁移

记录一下升级公共盘及数据迁移的经历 一、背景: 因旧公共盘性能较差,故公司打算采购一批主机,用于替换旧的公共盘。 二、迁移计划: 选择了用户使用较少的星期五晚上到星期日晚上作为迁移时间窗口,同时选择了fastco…

消息队列(一)消息队列的工作流程

什么是消息队列 首先,代入一个场景,我现在做一个多系统的集成,分别有系统A、B、C、D四个系统,A系统因为使用产生了业务数据,B、C、D需要使用这些数据做相关的业务处理和运算,最基本的做法就是通过接口通信…

SAP配置网络打印机

1、网络打印机 在项目中需要用打印机打印表单,如果不配置网络打印机,通常需要通过连接特定的计算机(如打印服务器)来进行打印,或者使用本地连接的打印机,这样每台计算机可能需要单独配置打印机或安装驱动&…

GB/T34944-2017 《Java语言源代码漏洞测试规范》解读——安全功能

GB/T34944-2017 《Java语言源代码漏洞测试规范》标准是软件测试实验室开展代码测试活动的重要依据,也是软件测试实验室申请代码测试CNAS/CMA实验室认证时所依据的标准方法。本系列文章一起解读GB/T34944-2017 《Java语言源代码漏洞测试规范》,前面的文章…

NLP 中文拼写检测纠正论文 C-LLM Learn to CSC Errors Character by Character

拼写纠正系列 NLP 中文拼写检测实现思路 NLP 中文拼写检测纠正算法整理 NLP 英文拼写算法,如果提升 100W 倍的性能? NLP 中文拼写检测纠正 Paper java 实现中英文拼写检查和错误纠正?可我只会写 CRUD 啊! 一个提升英文单词拼…

监听器listener

文章目录 监听器( listener)对Application内置对象监听的语法和配置对session内置对象监听的语法和配置 监听器( listener) 对象与对象的关系: 继承关联 tomcat一启动创建的顺序:监听器,config,application(全局初始化参数)&am…

LabVIEW数字式气压计自动检定系统

开发了一个基于LabVIEW开发的数字式气压计自动检定系统。在自动化检定PTB220和PTB210系列数字气压计,通过优化硬件组成和软件设计,实现高效率和高准确度的检定工作,有效降低人力成本并提升操作准确性。 项目背景 随着自动气象站的广泛部署&a…

在linux系统中使用jdbc访问sqlite数据库时报错“java.lang.UnsatisfiedLinkError”

1. 异常描述 在linux系统中使用jdbc访问sqlite数据库时出现如下错误提示: 2. 异常分析 可能是当前使用版本的sqlite-jdbc-xxx.jar版本有bug。 3. 异常解决 我是从3.8.9.1版本换到了3.16.1版本就好了。

Java(Sprigboot) 项目调用第三方 WebService 接口实现方式

文章目录 Java(Sprigboot) 项目调用第三方 WebService 接口实现方式WebService 简介业务场景描述WSDL 文档请求地址及方式接口请求/响应报文 代码实现1、接口请求/响应报文 JSON 准备(1)TransData(2)BaseInfo、InputData、OutputD…

基础爬虫案例实战

我们已经学习了多进程、requests、正则表达式的基本用法,但还没有完整地实现过一个爬取案例。这一节,我们就来实现一个完整的网站爬虫,把前面学习的知识点串联起来,同时加深对这些知识点的理解。 准备工作 我们需要先做好如下准备工作。 安…

Unittest02|TestSuite、TestRunner、HTMLTestRunner、处理excel表数据、邮件接收测试结果

目录 八、测试套件TestSuite和测试运行器TestRunner 1、基本概念 2、创建和使用测试套件 3、 自动发现测试用例、创建测试套件、运行测试 4、生成html的测试报告:HTMLTestRunner 1️⃣导入HTMLTestRunner模块 2️⃣运行测试用例并生成html文件 九、unittest…

C++-----线性结构

C线性结构模板 概念:线性结构是一种数据元素之间存在一对一线性关系的数据结构,如数组、链表、栈、队列等。C中的模板可以让我们编写通用的代码,适用于不同的数据类型,而不必为每种数据类型都重复编写相同的代码结构。作用&#…

六大基础深度神经网络之CNN

左侧是传统卷积网络输入的是一列像素点,右侧是卷积神经网络,输入的是具有长宽通道数的原始图像 下图为整体架构。卷积层可以认为提取特征,池化层是压缩特征。全连接层是把图像展平然后计算10个类别的概率值 给出一张图像不同区域的特征不同&a…

AIOps平台的功能对比:如何选择适合的解决方案?

定义与概念 AIOps,即人工智能运维(Artificial Intelligence for IT Operations),是将人工智能技术应用于 IT 运维领域,以实现自动化、智能化的运维管理。它通过整合大数据、机器学习等先进技术,对海量运维数…

pr基础(3)-lumetri

这篇应该是基础的完结篇了,主要记录调色方面的相关知识。 主要的内容是lumetri的使用 Lumetri的翻译其实来源于两个部分的组合。"Lume"这个词源于拉丁语,意为明亮度或光亮度,是Luminance (Luma) 的缩写,强调了光线的强…

【 thefuck 安装与使用】Linux 终端自动纠错工具:一头GitHub上的“草泥马“ - thefuck,妈妈再也不用担心我打错命令行了!

目录 快速安装使用 . 1.简介 2.安装 3.配置 4.补充 官方盗料参考 快速安装使用 快速安装使用,四步即可: #Ubuntu/Debian系统 sudo apt update sudo apt install python3-dev python3-pip sudo pip3 install thefuck #编辑bashrc配置文件 vim ~/.bashrc…

基于pytorch的深度学习基础3——模型创建与nn.Module

三 模型创建与nn.Module 3.1 nn.Module 模型构建两要素: 构建子模块——__init()__拼接子模块——forward() 一个module可以有多个module; 一个module相当于一个运算,都必须实现forward函数; 每一个mod…