一:背景

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

聊一聊 C#后台线程 如何阻塞程序退出的更多相关文章

  1. Qt中运行后台线程不阻塞UI线程的方案

    有一个想法,一个客户端,有GUI界面的同时也要向网络服务器发送本地采集的数据,通过网络发送数据的接口是同步阻塞的,需要等待服务器响应数据. 如果不采用后台线程的方案,用主UI线程关联一个定时器QTim ...

  2. C# 使用多线程,在关闭窗体时 怎么关闭窗体的所有线程,使程序退出。

    this.Close();   只是关闭当前窗口,若不是主窗体的话,是无法退出程序的,另外若有托管线程(非主线程),也无法干净地退出: Application.Exit();  强制所有消息中止,退出 ...

  3. C# 前台线程与后台线程区别

    using System; using System.Drawing; using System.Windows.Forms; using System.Threading; namespace Wi ...

  4. C#.Net前台线程与后台线程的区别

    本文来自:http://www.cnblogs.com/zfanlong1314/archive/2012/02/26/2390455.html .Net的公用语言运行时(Common Languag ...

  5. gunicorn结合django启动后台线程

    preload 为True的情况下,会将辅助线程或者进程开在master里,加重master的负担(master最好只是用来负责监听worker进程) django应用的gunicorn示例:只在主线 ...

  6. 通过使用Web Workers,Web应用程序可以在独立于主线程的后台线程中,运行一个脚本操作。这样做的好处是可以在独立线程中执行费时的处理任务,从而允许主线程(通常是UI线程)不会因此被阻塞/放慢。

    Web Workers API - Web API 接口参考 | MDNhttps://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API ...

  7. asp.net的web程序适用后台线程做异步任务

    耗时的任务适用异步线程来完成. 像记录日志,发邮件,发短信,通知其他系统等.可以进行封装,让后台线程来完成,不过最好适用消息队列,redis缓存等中间件,适用定时任务来做.可以保证消息不丢失. usi ...

  8. C# WinForm 多线程 应用程序退出的方法 结束子线程

    1.this.Close(); 只是关闭当前窗口,若不是主窗体的话,是无法退出程序的,另外若有托管线程(非主线程),也无法干净地退出: 2.Application.Exit(); 强制所有消息中止,退 ...

  9. exe崩溃用windbgattach后有宝贵现场,可看程序退出线程等,千万不要清屏

    exe崩溃用windbgattach后有宝贵现场,可看程序退出线程等,千万不要清屏

  10. C# Winform 窗体界面”假死”后台线程阻塞 解决办法–BeginInvoke

    原文:C# Winform 窗体界面"假死"后台线程阻塞 解决办法–BeginInvoke 这个方法可以用在任何后台任务耗时较长,造成界面“假死”界面控件不更新的情况. 比如要要执 ...

随机推荐

  1. ftrace在应用上的使用

    之前介绍通过命令行配置和使用ftrace功能,但是实际中,我们也会希望抓C/C++程序中某段代码的调度情况.笔者前不久就遇到这种问题,某个函数调用时延概率超过100ms,是为什么?这时候就需要在他们代 ...

  2. NOI 2023 (简单题)

    Day1 T1 方格染色(color) 容易发现相对难处理的是斜线,但是我们发现斜线不超过 \(5\) 条,那么对于这一部分我们可以拆贡献,然后暴力做. 具体而言,先算出斜线减去横/竖线的面积,再算出 ...

  3. java工具篇-IDEA

    java的开发离不开好的开发工具,这就需要了解集成开发工具idea 背景黑白风格 设置方法File–>settings–>Appearance & Behavior–>App ...

  4. ARM 版 OpenEuler 22.03 部署 KubeSphere v3.4.0 不完全指南续篇

    作者:运维有术 前言 知识点 定级:入门级 KubeKey 安装部署 ARM 版 KubeSphere 和 Kubernetes ARM 版 KubeSphere 和 Kubernetes 常见问题 ...

  5. T3 出行云原生容器化平台实践

    公司简介 T3 出行是南京领行科技股份有限公司打造的智慧出行生态平台,由中国第一汽车集团有限公司.东风汽车集团有限公司.重庆长安汽车股份有限公司发起,联合腾讯.阿里巴巴等互联网企业共同投资打造.公司以 ...

  6. 低配置PC环境下的魔兽世界游戏体验:ToDesk云电脑性能测试分析

    近期魔兽世界再度开服,吸引了众多游戏老玩家回归.然而随着游戏内容的不断更新,其对电脑配置的要求也在逐渐升提高.对于许多电脑配置较低的老玩家,如何在不升级硬件的情况下流畅运行魔兽世界成为了一个难题. 随 ...

  7. Go服务自动触发单元测试覆盖率

    一.用到的工具 Gitlab Jenkins Shell go test 二.实现原理 在gitlab上配置jenkins的webhook,当有代码变更时自动触发jenkins构建job,job内的s ...

  8. 9-5 额外的string操作

    9.5.1 构造string的其他方法:略 9.5.2 改变string的其他方法:略 9.5.3 string搜索操作:略 9.5.4 compare函数:略 9.5.5 数值转换 int main ...

  9. Minio安装以及使用

    Minio介绍 MinIO 是一个基于Apache License v2.0开源协议的对象存储服务.适合于存储大容量非结构化的数据,例如图片.视频.日志文件.备份数据和容器/虚拟机镜像等,而一个对象文 ...

  10. Git Flow开发分支管理

    Git Flow Git Flow 是一种基于 Git 版本控制系统的分支管理模型,定义了一套严格的分支命名和操作规范 主要包括以下几种分支类型: 主干分支(master):始终保持稳定,只包含经过充 ...