当我从 .NET 生成一个新线程时到底发生了啥?

Posted

技术标签:

【中文标题】当我从 .NET 生成一个新线程时到底发生了啥?【英文标题】:What is exactly happening when I spawn a new thread from .NET?当我从 .NET 生成一个新线程时到底发生了什么? 【发布时间】:2011-09-12 03:17:38 【问题描述】:

当我在 .NET 中生成一个新线程时,我想了解幕后究竟发生了什么,如下所示:

Thread t = new Thread(DoWork); //I am not interested in DoWork per se
t.Start();

1。在 CLR 和 Windows 内核中创建了哪些与线程相关的对象? 2. 为什么需要这些对象? 3. 在 x86、x64 Windows 上分配了多少托管/非托管内存(堆和栈)?

更新 我正在寻找诸如托管线程对象之类的对象,我假设它是t,但也许还有其他一些其他托管对象; 内核线程对象用户线程环境块等。

非常感谢!

【问题讨论】:

【参考方案1】:

分配的 Win32 和内核内存

我不确定 .NET 部分是如何工作的,但如果运行时确实决定使用操作系统创建一个真正的线程,它最终会调用 kernel32.dll 中的 Win32 API CreateThread,可能来自 mscorlib。 ni.dll

默认情况下,新线程会为堆栈获取 1MB 的虚拟地址,并根据需要提交。这可以通过maxStackSize parameter 进行控制。主线程的栈大小来自于可执行文件本身的一个参数。

在进程的地址空间中,将分配一个TEB(线程环境块)(see also)。顺便说一句,x86 上的 FS 寄存器指向这个,用于线程本地存储和结构化异常处理 (SEH)。可能还有其他由 Win32 分配的没有记录的东西。

在创建 Win32 线程时,会联系 Win32 服务器进程 (csrss.exe)。您可以看到 csrss 已在 Process Explorer 中为所有 Win32 进程和线程打开了句柄,以进行某种记账。

进程中加载​​的 DLL 将收到新线程的通知,并可能分配自己的内存来跟踪线程。

内核将从内核非分页池中创建一个ETHREAD [layout](派生自KTHREAD)对象来跟踪线程的状态。还会分配一个内核堆栈(x86 默认为 12k),可以分页(除非线程处于内核模式等待状态)。

为什么这么多东西需要为一个线程分配内存

线程是操作系统提供的最小的抢占式调度单元,并且有很多上下文连接到它们。许多不同的组件需要为每个线程提供单独的上下文,因为系统服务需要能够处理多个线程同时执行不同的操作。

有些服务需要您明确地向它们声明新线程,但大多数服务都应该自动使用新线程。有时这意味着在线程启动时分配空间。当线程参与其他服务时,用于跟踪线程的内存量会随着这些服务为线程设置自己的上下文而增加。

分配了多少内存

很难说为线程分配了多少内存,因为它分布在多个地址空间和堆中。它会因 Windows 版本、安装的组件以及当前加载到进程中的内容而异。

通常认为最大的成本是新线程默认使用的 1MB 地址空间,但即使是这个限制,也可以允许在单个进程中使用数百个地址空间而不会耗尽空间。

如果设计使用的操作系统线程数多于系统中的 CPU 数量,则应进行审查。具有线程池的工作队列和具有用户模式调度的轻量级线程(使用纤程或其他库的实现)应该能够处理多线程,而不需要过多的操作系统线程,从而使线程的内存成本变得不重要。

【讨论】:

【参考方案2】:

所以这是一个非常复杂的问题,实际上并没有很好的“x”答案。

    CLR 不需要将单个 CLR 线程映射到单个 OS 光纤。所以……这很难回答。我认为当前版本的 .NET (4.0) 尝试在所有操作系统上尽可能使用 CLR 线程和操作系统纤程之间的一对一关系。 .NET 的早期版本(更像 Thread 对象的一部分。如果深入研究 IL,您会看到许多实际执行的内部调用。 我认为问题是“为什么需要这些对象?”如果是这样,那是因为操作系统主机实际上必须拥有光纤才能在其上执行该线程的代码。 ThreadPool 的使用可以大大降低每次创建它们的成本。 抱歉……视情况而定。其中很多也是不受管理的,这意味着操作系统主机可以根据负载和系统版本选择不同的处理方式。

“控制线程的逻辑抽象由类库中System.Threading.Thread 对象的实例捕获。” http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-335.pdf

所以 EMCA 标准确实没有说明这个话题。但幸运的是我们有......

“因为 CLR 线程对象是 per-fiber,所以挂在它上面的任何信息也是 per-fiber。Thread.ManagedThreadId 返回一个稳定的 ID,与 CLR 线程一起流动。它不依赖于物理操作系统线程,这意味着使用它意味着没有任何形式的关联。在同一线程上运行的不同纤程返回不同的 ID。”来自 Joe Duffy http://www.bluebytesoftware.com/blog/2006/11/10/FibersAndTheCLR.aspx

【讨论】:

您确定 CLR 使用光纤吗?我对此表示怀疑。纤维是事后才添加到 NT 内核中的,很少使用,但线程(NT 内核的)是 NT 派生操作系统中的主要线程机制。此外,我倾向于认为 CLR 线程将 1 对 1 映射到内核线程,因为您始终可以从 CLR 线程对象获取线程 ID(甚至句柄)。 我用 Joe Duffy 的一些支持信息编辑了我的答案。我希望这能解决问题。 谢谢。您提到的博客条目已经很老了。 CLR 版本 4 可能会继续使用纤程,但 BeginThreadAffinity/EndThreadAffinity 绝对可以让您将特定的 CLR 线程“粘合”到内核线程。 确实如此,但这并不意味着如果您具有亲和力的线程处于空闲状态并且系统处于负载状态,则不能被另一根纤程使用。是的,博客条目很旧,但是从那以后我没有听到任何与之相矛盾的东西。 +1:这似乎是一个全面的答案。如果 CLR 使用纤程,尤其是在 SQL Server 是主机的情况下,我一点也不感到惊讶。【参考方案3】:

看here;托管(即 CLR)原语和非托管(即 NT 内核)原语之间存在映射,可以回答您的大部分问题。

【讨论】:

以上是关于当我从 .NET 生成一个新线程时到底发生了啥?的主要内容,如果未能解决你的问题,请参考以下文章

当我读/写文件时(在操作系统级别)发生了啥?

java.net.SocketInputStream.socketRead0(Native Method) 上挂了一个线程,有人知道发生了啥吗?

当我创建一个与访问中的其他查询一起使用的查询时,幕后发生了啥?

C++ 这里到底发生了啥? [关闭]

我迷路了。 ASP.NET MVC 5 发生了啥?

调试 JSF 生命周期——每个阶段到底发生了啥