【问题标题】:What would cause so many unstarted threads?什么会导致这么多未启动的线程?
【发布时间】:2014-07-10 02:33:25
【问题描述】:

现在我很奇怪地遇到了一个错误。

我的应用程序是一个需要使用 WCF 连接到服务器的 winform 客户端。我的应用程序会引用几个 .net 和 c++ 模块/dll。

出于某种原因,我在我的代码中设置了ThreadPool.SetMaxThreads(150, 200)。运行几个小时后,此客户端将与服务器断开连接。

用windbg调试后发现线程池里已经塞满了很多奇怪的线程。所以线程池中无法创建新线程,我认为WCF也无法创建线程来连接导致断开的服务器。

这些奇怪的线程看起来像这样:

                                                                         Lock  
      ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt
XXXX   3  cb8 0043afd8      1400 Preemptive  00000000:00000000 003f3248 0     Ukn 

根据Yun Jin's WebLog "Thread, System.Threading.Thread, and !Threads" seriesSSCLI 2.0 source code,产生这些线程的最大概率是CLR在线程池中创建了一个新线程,并且该线程永远不会被恢复。

我想知道为什么或如何恢复一个线程或很多线程失败

以下是更多技术细节:

当CLR在线程池中创建一个新线程时,会调用SetupUnstartedThread方法和CreateNewThread/CreateNewOSThread方法。

SetupUnstartedThread之后,CLR会创建一个这样的线程

                                                                         Lock  
      ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt
XXXX   3    0 0043afd8      1400 Preemptive  00000000:00000000 003f3248 0     Ukn 

具有0x1400 (TS_Unstarted | TS_WeOwn) 状态且没有 OSID 且没有调试器 ID (XXXX)

CreateNewThread/CreateNewOSThread之后,线程会变成

                                                                         Lock  
      ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt
XXXX   3  cb8 0043afd8      1400 Preemptive  00000000:00000000 003f3248 0     Ukn 

哪个有 OSID 也没有调试器 ID (XXXX)

此外,线程的ExposedObject 字段为空。

但是如果线程恢复成功,这意味着ntdll!LdrInitializeThunk被调用,线程将获得调试器ID (2)

                                                                         Lock  
   ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt
2   3  cb8 0043afd8      1400 Preemptive  00000000:00000000 003f3248 0     Ukn 

线程的状态与错误状态不同(没有调试器ID)

编辑给 Thomas W

如果你提到的选项 c 是

(c) CLR 中可能运行托管代码的特殊操作系统线程。

根据SSCLI 2.0 source code,如果OS线程要访问托管代码,CLR将调用SetupThread方法,该方法将运行以下代码

// reset any unstarted bits on the thread object
FastInterlockAnd((ULONG *) &pThread->m_State, ~Thread::TS_Unstarted);
FastInterlockOr((ULONG *) &pThread->m_State, Thread::TS_LegalToJoin);

这绝对不是0x1400

任何奇怪的线程在~线程列表中都没有对应的线程。所以你在!runaway看不到他们

编辑 2

很抱歉这么晚才更新这篇文章。没有找到根本原因,但找到了解决方法,即将 .Net Framework 4.0 替换为 .Net Framework 4.5

以下内容将详细介绍如何找到解决方法。

曾几何时,我跟踪过这些奇怪线程的整个生命周期。我们都知道 CLR 中有一个Gate Thread (thread help to monitor status of completion port threads and worker threads, only one)。当我的应用程序开始出错时,Gate Thread 会调用 clr!ThreadpoolMgr::CreateWorkerThread 周期性,这将创建一个新的 clr 线程对象和一个新的 os 线程对象。

0:004> k
ChildEBP RetAddr  
04c8f6f8 6f3ea8ff KERNEL32!CreateThreadStub
04c8f744 6f3ea77b clr!Thread::CreateNewOSThread+0xba
04c8f78c 6f3eabc1 clr!Thread::CreateNewThread+0xa9
04c8f81c 6f4a6aed clr!ThreadpoolMgr::CreateUnimpersonatedThread+0xbb
04c8f83c 6f4a560e clr!ThreadpoolMgr::CreateWorkerThread+0x19
04c8f864 6f4a4457 clr!ThreadpoolMgr::EnsureEnoughWorkersWorking+0x116
04c8f94c 75973c45 clr!ThreadpoolMgr::GateThreadStart+0x431
04c8f958 771a37f5 KERNEL32!BaseThreadInitThunk+0xe
04c8f998 771a37c8 ntdll!__RtlUserThreadStart+0x70
04c8f9b0 00000000 ntdll!_RtlUserThreadStart+0x1b

新线程看起来像这样

                                                                         Lock  
      ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt
XXXX   3  cb8 0043afd8      1400 Preemptive  00000000:00000000 003f3248 0     Ukn 

我猜到这个帖子可能永远不会恢复了。原来我错了。过了一会儿,这个线程分别调用了ntdll!LdrInitializeThunkntdll!_RtlUserThreadStart

0:065> k
ChildEBP RetAddr  
1d54f7c0 75973c45 clr!Thread::intermediateThreadProc
1d54f7cc 771a37f5 KERNEL32!BaseThreadInitThunk+0xe
1d54f80c 771a37c8 ntdll!__RtlUserThreadStart+0x70
1d54f824 00000000 ntdll!_RtlUserThreadStart+0x1b
                                                                         Lock  
      ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt
  65   3  cb8 0043afd8      1400 Preemptive  00000000:00000000 003f3248 0     Ukn 

查看clr!Thread::intermediateThreadProc的参数后,发现这个线程会调用clr!ThreadpoolMgr::WorkerThreadStart

然后神奇的事情发生了。

clr!ThreadpoolMgr::WorkerThreadStart 结束后,通常clr!ThreadStore::RemoveThread 应该在线程死亡之前由 Finalizer 线程 调用。 但这次没有。

没有clr!ThreadStore::RemoveThread,只是

0:065> k
ChildEBP RetAddr  
1889fb04 7716f73a ntdll!LdrpCallInitRoutine+0x14
1889fba8 7716f63b ntdll!LdrShutdownThread+0xe6
1889fbb8 75973c4c ntdll!RtlExitUserThread+0x2a
1889fbc4 771a37f5 KERNEL32!BaseThreadInitThunk+0x15
1889fc04 771a37c8 ntdll!__RtlUserThreadStart+0x70
1889fc1c 00000000 ntdll!_RtlUserThreadStart+0x1b

所以对应的os线程已经被销毁,但是clr线程也存在。

                                                                         Lock  
      ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt
XXXX   3  cb8 0043afd8      1400 Preemptive  00000000:00000000 003f3248 0     Ukn 

也许你会问为什么线程的状态没有改变。出于某种原因,当时我还没有深入了解clr!ThreadpoolMgr::WorkerThreadStart。所以不能给你答案,不过我也看了SSCLI 2.0 source code又再猜了一遍(^_^)。

clr!ThreadpoolMgr::WorkerThreadStart 将调用 'clr!SetupThreadPoolThreadNoThrow'。以下是 'clr!SetupThreadPoolThreadNoThrow' 的代码 sn-p。

EX_TRY
{
    pThread = SetupThreadPoolThread(typeTPThread);
}
EX_CATCH
{
    if (pHR)
    {
        *pHR = GET_EXCEPTION()->GetHR();
    }
}
EX_END_CATCH(SwallowAllExceptions);

请注意“SwallowAllExceptions”。然后你可以看到这个方法会调用clr!SetupThreadPoolThread。再次显示代码 sn-p。

if (NULL == (pThread = GetThread()))
{
    pThread = SetupInternalThread();
}
if ((pThread != NULL) && ((pThread->m_State & Thread::TS_ThreadPoolThread) == 0))
{

    if (typeTPThread == WorkerThread)
    {
        FastInterlockOr((ULONG *) &pThread->m_State, Thread::TS_ThreadPoolThread | Thread::TS_TPWorkerThread);
    }
    else if (typeTPThread == CompletionPortThread)
    {
        FastInterlockOr ((ULONG *) &pThread->m_State, Thread::TS_ThreadPoolThread | Thread::TS_CompletionPortThread);
    }
    else
    {
        FastInterlockOr((ULONG *) &pThread->m_State, Thread::TS_ThreadPoolThread);
    }
}

那我猜如果调用clr!SetupInternalThread的时候出现异常,线程的状态就没有机会改变了。

所以这是我第一次认为 .net 框架中可能存在一个由我的应用程序触发的小缺陷。同时,我的一位同事告诉我,他无法重现该错误。查了一下他的环境,发现他用的是.Net Framework 4.5

到目前为止,升级.net框架后该bug没有再次出现。

【问题讨论】:

  • 也许您的代码中有错误(您还没有发布!)
  • 我知道我的代码中存在错误,但我不知道在哪里并且需要一些线索。现在我已经将问题跟踪到了winapi层,我想知道我的代码将如何影响线程。
  • 选项 c)“用于表示现在已死的活动 OS 线程”怎么样?本地的状态是什么? ~ 说什么? !runaway 会改变吗?
  • 我在主帖中添加了评论
  • 我在主帖中添加了另一条评论。找到根本原因后,我会再次刷新帖子。

标签: .net multithreading windbg


【解决方案1】:

SSCCE 用于分析线程

要查看 .NET 如何创建托管线程并将其标记为 XXX,您可以运行以下代码。在 Debug build 中编译应用程序,启动 WinDbg 并在调试器下运行应用程序。在初始断点处,运行以下命令:

sxe -c ".loadby sos clr;g" ld clr.dll;.ocommand OCOMMAND;g

然后应用程序将自行调试,您将看到线程发生变化

Step                .NET threads  Unstarted  Dead     Thread objects  Native threads
1 (before started)  2             0          0        1               4
2 (Thread started)  3             1 (XXX)    0        2               5
3 (Thread running)  3             0          0        3               8
4 (Thread ended)    3             0          1 (XXX)  2               7
5 (GC ran)          3             0          1 (XXX)  2               4

SSCCE 代码:

using System;
using System.Diagnostics;
using System.Threading;

namespace ManagedThreadDebug
{
    class Program
    {
        static void Main()
        {
            InformDebug("Before creating thread object.");

            var t = new Thread(ThreadRun);
            InformDebug("After creating thread object and calling Start().");

            t.Start();
            InformDebug("While thread is running.");

            t.Join();
            InformDebug("After thread was running (GC potentially not run yet).");

            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            Thread.Sleep(10);
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            Thread.Sleep(10);
            InformDebug("After thread was running (GC hopefully ran).");
        }

        private static void ThreadRun()
        {
            Thread.Sleep(1000);
        }

        private static void InformDebug(string message)
        {
            Console.WriteLine(message);
            Trace.WriteLine("OCOMMAND .echo >>> "+message+";!threads;.echo;!dumpheap -stat -type Thread;.echo;~;g");
        }
    }
}

几乎完整的输出,为了简洁而缩短:

>>> Before creating thread object.
ThreadCount:      2
UnstartedThread:  0
BackgroundThread: 1
PendingThread:    0
DeadThread:       0
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   0    1 1074 00441310     2a020 Preemptive  02796F48:00000000 00408378 1     MTA 
   2    2 1fb8 00411258     2b220 Preemptive  00000000:00000000 00408378 0     MTA (Finalizer) 

Statistics:
      MT    Count    TotalSize Class Name
69f02e64        1           52 System.Threading.Thread

.  0  Id: b78.1074 Suspend: 1 Teb: 7efdd000 Unfrozen
   1  Id: b78.2194 Suspend: 1 Teb: 7efda000 Unfrozen
   2  Id: b78.1fb8 Suspend: 1 Teb: 7efd7000 Unfrozen
   3  Id: b78.1500 Suspend: 1 Teb: 7efaf000 Unfrozen

>>> After creating thread object and calling Start().
ThreadCount:      3
UnstartedThread:  1
BackgroundThread: 1
PendingThread:    0
DeadThread:       0
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   0    1 1074 00441310     2a020 Preemptive  02797334:00000000 00408378 1     MTA 
   2    2 1fb8 00411258     2b220 Preemptive  00000000:00000000 00408378 0     MTA (Finalizer) 
XXXX    3    0 00474900      1400 Preemptive  00000000:00000000 00408378 0     Ukn 

Statistics:
      MT    Count    TotalSize Class Name
69f02e64        2          104 System.Threading.Thread

.  0  Id: b78.1074 Suspend: 1 Teb: 7efdd000 Unfrozen
   1  Id: b78.2194 Suspend: 1 Teb: 7efda000 Unfrozen
   2  Id: b78.1fb8 Suspend: 1 Teb: 7efd7000 Unfrozen
   3  Id: b78.1500 Suspend: 1 Teb: 7efaf000 Unfrozen
   4  Id: b78.27d8 Suspend: 1 Teb: 7efac000 Unfrozen

>>> While thread is running.
ThreadCount:      3
UnstartedThread:  0
BackgroundThread: 1
PendingThread:    0
DeadThread:       0
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   0    1 1074 00441310     2a020 Preemptive  02797550:00000000 00408378 1     MTA 
   2    2 1fb8 00411258     2b220 Preemptive  00000000:00000000 00408378 0     MTA (Finalizer) 
   6    3 1d04 00474900     2b020 Preemptive  00000000:00000000 00408378 1     MTA 

Statistics:
      MT    Count    TotalSize Class Name
69f02e64        2          104 System.Threading.Thread

.  0  Id: b78.1074 Suspend: 1 Teb: 7efdd000 Unfrozen
   1  Id: b78.2194 Suspend: 1 Teb: 7efda000 Unfrozen
   2  Id: b78.1fb8 Suspend: 1 Teb: 7efd7000 Unfrozen
   3  Id: b78.1500 Suspend: 1 Teb: 7efaf000 Unfrozen
   4  Id: b78.27d8 Suspend: 1 Teb: 7efac000 Unfrozen
   5  Id: b78.2478 Suspend: 1 Teb: 7efa9000 Unfrozen
   6  Id: b78.1d04 Suspend: 1 Teb: 7efa6000 Unfrozen
   7  Id: b78.1fdc Suspend: 1 Teb: 7efa3000 Unfrozen

 >>> After thread was running (GC potentially not run yet).
ThreadCount:      3
UnstartedThread:  0
BackgroundThread: 1
PendingThread:    0
DeadThread:       1
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   0    1 1074 00441310     2a020 Preemptive  027977FC:00000000 00408378 1     MTA 
   2    2 1fb8 00411258     2b220 Preemptive  00000000:00000000 00408378 0     MTA (Finalizer) 
XXXX    3    0 00474900     39820 Preemptive  00000000:00000000 00408378 0     Ukn 

Statistics:
      MT    Count    TotalSize Class Name
69f02e64        2          104 System.Threading.Thread

.  0  Id: b78.1074 Suspend: 1 Teb: 7efdd000 Unfrozen
   1  Id: b78.2194 Suspend: 1 Teb: 7efda000 Unfrozen
   2  Id: b78.1fb8 Suspend: 1 Teb: 7efd7000 Unfrozen
   3  Id: b78.1500 Suspend: 1 Teb: 7efaf000 Unfrozen
   4  Id: b78.27d8 Suspend: 1 Teb: 7efac000 Unfrozen
   5  Id: b78.2478 Suspend: 1 Teb: 7efa9000 Unfrozen
   7  Id: b78.1fdc Suspend: 1 Teb: 7efa3000 Unfrozen

>>> After thread was running (GC hopefully ran).
ThreadCount:      3
UnstartedThread:  0
BackgroundThread: 1
PendingThread:    0
DeadThread:       1
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   0    1 1074 00441310     2a020 Preemptive  02797380:00000000 00408378 1     MTA 
   2    2 1fb8 00411258     2b220 Preemptive  00000000:00000000 00408378 0     MTA (Finalizer) 
XXXX    3    0 00474900     39820 Preemptive  00000000:00000000 00408378 0     Ukn 

Statistics:
      MT    Count    TotalSize Class Name
69f02e64        2          104 System.Threading.Thread

.  0  Id: b78.1074 Suspend: 1 Teb: 7efdd000 Unfrozen
   1  Id: b78.2194 Suspend: 1 Teb: 7efda000 Unfrozen
   2  Id: b78.1fb8 Suspend: 1 Teb: 7efd7000 Unfrozen
   3  Id: b78.1500 Suspend: 1 Teb: 7efaf000 Unfrozen

结论

显示为 XXXX 的线程可以是未启动或死线程。您可能不会喜欢这个答案:除非您向我们展示一些代码,否则无法知道这些线程来自何处。潜在候选人:

  • 代码中的新 Thread() 语句
  • Parallel.For 和类似的使用
  • 使用线程池
  • 第三方库中的代码

调试线程启动和退出

在 WinDbg 中运行应用程序,并在线程启动或线程退出时停止。

sxe ct;sxe et

然后查看发生这种情况的位置并特别检查创建线程的代码。如果这还不够具体,您还可以尝试在 .NET 线程方法上设置断点。

【讨论】:

  • 谢谢!我将使用此方法检查我的代码。如果我取得一些进展,我会更新这篇文章。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-04-06
  • 1970-01-01
  • 2013-02-25
  • 2014-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多