IT评测·应用市场-qidao123.com

标题: 聊一聊 C#前台线程 怎样阻塞程序退出 [打印本页]

作者: 曹旭辉    时间: 2024-12-22 16:17
标题: 聊一聊 C#前台线程 怎样阻塞程序退出
一:背景

1. 讲故事

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

1. 测试代码

为了方便讲解,先上一段代码,参考如下:
  1.     static void Main(string[] args)
  2.     {
  3.         var thread = new Thread(() =>
  4.         {
  5.             while (true)
  6.             {
  7.                 Console.WriteLine(DateTime.Now);
  8.             }
  9.         });
  10.         thread.IsBackground = false;
  11.         thread.Start();
  12.     }
复制代码

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

要想知道这个答案,可以用 windbg 附加一下看看主线程此时正在做什么? 参考如下:
  1. 0:000> k
  2. # Child-SP          RetAddr               Call Site
  3. 00 0000003f`7d59e498 00007ffd`cd8d0590     ntdll!NtWaitForMultipleObjects+0x14
  4. 01 0000003f`7d59e4a0 00007ffd`8f842dd4     KERNELBASE!WaitForMultipleObjectsEx+0xf0
  5. 02 (Inline Function) --------`--------     coreclr!Thread::DoAppropriateAptStateWait+0x4a [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 3333]
  6. 03 0000003f`7d59e790 00007ffd`8f842c25     coreclr!Thread::DoAppropriateWaitWorker+0x170 [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 3467]
  7. 04 0000003f`7d59e850 00007ffd`8f99498e     coreclr!Thread::DoAppropriateWait+0x85 [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 3182]
  8. 05 (Inline Function) --------`--------     coreclr!CLREventBase::WaitEx+0x26 [D:\a\_work\1\s\src\coreclr\vm\synch.cpp @ 459]
  9. 06 (Inline Function) --------`--------     coreclr!CLREventBase::Wait+0x26 [D:\a\_work\1\s\src\coreclr\vm\synch.cpp @ 412]
  10. 07 0000003f`7d59e8d0 00007ffd`8f94c185     coreclr!CLREventWaitWithTry+0x9a [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 5676]
  11. 08 0000003f`7d59e980 00007ffd`8f8a062b     coreclr!ThreadStore::WaitForOtherThreads+0xabafd [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 5715]
  12. 09 0000003f`7d59e9b0 00007ffd`8f83eaad     coreclr!RunMainPost+0x5f [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1407]
  13. 0a 0000003f`7d59e9f0 00007ffd`8f83e0e7     coreclr!Assembly::ExecuteMainMethod+0x1f5 [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1524]
  14. 0b 0000003f`7d59ecc0 00007ffd`8f889778     coreclr!CorHost2::ExecuteAssembly+0x267 [D:\a\_work\1\s\src\coreclr\vm\corhost.cpp @ 349]
  15. ...
复制代码
从卦中数据可以看到,主线程正在调用 ThreadStore::WaitForOtherThreads 方法,貌似是在等待其他线程完成,那具体做了什么呢?这个需要在 coreclr 上寻找答案,删减后的代码如下:
  1.     void ThreadStore::WaitForOtherThreads()
  2.     {
  3.         if (!OtherThreadsComplete())
  4.         {
  5.             TSLockHolder.Release();
  6.             pCurThread->SetThreadState(Thread::TS_ReportDead);
  7.             DWORD ret = WAIT_OBJECT_0;
  8.             while (CLREventWaitWithTry(&m_TerminationEvent, INFINITE, TRUE, &ret))
  9.             {
  10.             }
  11.         }
  12.     }
  13.     BOOL OtherThreadsComplete()
  14.     {
  15.         return (m_ThreadCount - m_UnstartedThreadCount - m_DeadThreadCount
  16.                 - Thread::m_ActiveDetachCount + m_PendingThreadCount
  17.                 == m_BackgroundThreadCount);
  18.     }
复制代码
从卦中看逻辑还黑白常简单的,就是由于 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++代码如下:
  1.     void Thread::SetBackground(BOOL isBack)
  2.     {
  3.         if (isBack)
  4.         {
  5.             if (!IsBackground())
  6.             {
  7.                 SetThreadState(TS_Background);
  8.                 if (!IsUnstarted())
  9.                     ThreadStore::s_pThreadStore->m_BackgroundThreadCount++;
  10.                 ThreadStore::CheckForEEShutdown();
  11.             }
  12.         }
  13.     }
  14.     void ThreadStore::CheckForEEShutdown()
  15.     {
  16.         if (g_fWeControlLifetime &&
  17.             s_pThreadStore->OtherThreadsComplete())
  18.         {
  19.             BOOL bRet;
  20.             bRet = s_pThreadStore->m_TerminationEvent.Set();
  21.             _ASSERTE(bRet);
  22.         }
  23.     }
复制代码
哈哈,卦中的化煞方法真的妙不可言,做了如下两个步骤:
假如有些朋侪没搞明白,我再画一张简图吧:

4. 判定线程的前后状态

这是最后一个要聊的话题,要想知道线程的前后状态,这个需要在 coreclr 源码中寻找答案,参考代码如下:
  1.     void SetThreadState(ThreadState ts)
  2.     {
  3.         InterlockedOr((LONG*)&m_State, ts);
  4.     }
  5.     enum ThreadState
  6.     {
  7.         TS_Background = 0x00000200,    // Thread is a background thread
  8.     }
复制代码
从代码中可以看到,只要判定 ThreadState 中有没有 0x200 的标记即可,接下来用 !t 观察线程状态。
  1. 0:000> !t
  2. ThreadCount:      4
  3. UnstartedThread:  0
  4. BackgroundThread: 3
  5. PendingThread:    0
  6. DeadThread:       0
  7. Hosted Runtime:   no
  8.                                                                                                             Lock  
  9. DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
  10.    0    1      918 000001FA530317B0  203a220 Preemptive  000001FA574096F8:000001FA5740A5C8 000001fa530273e0 -00001 MTA
  11.    6    2     37c8 000001FA53009B70    21220 Preemptive  0000000000000000:0000000000000000 000001fa530273e0 -00001 Ukn (Finalizer)
  12.    7    3     2c7c 000001FA5307F700    2b220 Preemptive  0000000000000000:0000000000000000 000001fa530273e0 -00001 MTA
  13.    8    4     3bd4 0000023AE951DFD0    2b020 Preemptive  000001FA57563A08:000001FA57565010 000001fa530273e0 -00001 MTA
复制代码
从卦中可以轻松的看到 DBG=8 的线程状态是 2b020,自然就是前台线程咯。
三:总结

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


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com (https://dis.qidao123.com/) Powered by Discuz! X3.4