堆栈分配

28

我正在深入研究内存模型,但在理解进程中有多少个堆时遇到了困难。

假设我们有一个包含 5 个线程的进程,那么我说这意味着我们有 5 个栈和 1 个堆,这是正确的吗?

如果是这样,那么线程能够访问彼此的栈吗(或者说这恰好是它们具有单独的栈的原因,以防止出现数据损坏),如果只有 1 个堆,显然它们都可以访问该堆,因此需要使用锁来支持多个线程?我的理解是否正确?

4个回答

41

是的,每个线程都有自己的堆栈。这是一个硬性要求,堆栈跟踪方法在完成后返回的位置,它存储返回地址。由于每个线程执行自己的代码,它们需要自己的堆栈。局部变量和方法参数也存储在其中,使它们(通常)是线程安全的。

堆的数量是一个更复杂的细节。你将垃圾收集堆计入1中。从实现角度来看,三代堆加上大对象堆是逻辑上不同的堆,加起来是四个。当你分配太多时,这个实现细节开始变得重要。

在托管代码中不能完全忽略的另一个是存储静态变量的堆。它与AppDomain关联,静态变量的生存期与AppDomain一样长。在.NET文献中通常称为“加载器堆”。它实际上由三个堆(高频、低频和存根堆)组成,jitted代码和类型数据也存储在其中,但这已经涉及到很细节的问题。

忽略列表更下面的是本机代码使用的堆。其中两个可以从Marshal类中轻松地看到。有一个默认的进程堆,Windows从中分配,Marshal.AllocHGlobal()也是如此。还有一个单独的堆,COM存储数据,Marshal.AllocCoTaskMem()从其中分配。最后,任何你与之进行互操作的本机代码都将拥有其自己的堆来支持其运行时。这种代码使用的堆的数量仅受到加载到进程中的本机DLL数量的限制。所有这些堆都存在,但你几乎从不直接处理它们。

所以,最少需要10个堆。


12

简而言之,是的

一个进程中的所有线程共享相同的堆,因此它们可以交换数据。每个线程都有自己的栈,与该线程上当前代码执行相关。

关于多线程的非常好的资源在这里:http://www.albahari.com/threading/

一个线程类似于您的应用程序运行的操作系统进程。就像进程在计算机上并行运行一样,线程在单个进程内并行运行。进程彼此完全隔离;线程只有有限的隔离程度。特别地,线程与在同一应用程序中运行的其他线程共享(堆)内存。这部分是多线程有用的原因之一:例如,一个线程可以在后台获取数据,而另一个线程可以在数据到达时显示数据。


4
线程是在单个进程的虚拟地址空间中同时运行的独立指令流。堆是系统为每个进程提供的一大块内存,用于其私有使用。进程可以调整其堆大小,并按需要使用堆空间。线程可以协作使用此堆空间,并且还可以分配其他私有内存区域,即所谓的“线程本地存储”(TLS)。
由于所有线程共享相同的虚拟地址空间,它们可以直接访问彼此的栈内存。这意味着一个线程可以将其栈上的变量作为参数传递给在其他线程中运行的函数。但是线程栈仍然是独立的,因为一个线程永远不会将值推送或弹出到其他线程的栈中,而只会推送到自己的栈空间中。由于x86和x86-64上的堆栈向下增长,因此每个线程的堆栈内存底部都有一个特殊页面 - 所谓的"保护页"。如果在操作堆栈时到达了保护页,则会发生堆栈故障。
在C和C++等未管理语言中,可以通过指针访问进程内存的任何部分。一个线程完全可以捣乱另一个线程的堆栈内容,从而使第二个线程(以及整个进程)崩溃。但是在C#中,除了unsafe块之外,这些情况不会发生,因为CLR管理了堆栈。

一个线程可以将其堆栈上的变量作为参数传递给在其他线程中运行的函数。你要如何准确地实现这个功能呢? - H H
使用Win32线程,您可以启动一个新线程并将当前线程中的本地变量地址作为线程参数传递。您还可以使用PostThreadMessage向已经运行的线程发送消息。 - Hristo Iliev
在 C/C++ 中,您可以通过传递变量的值或使用指针按地址传递变量。在 C++ 中,还可以使用别名来实现指针。 - Hristo Iliev

2
这是实现定义的,但让我们谈论一下最流行的现代操作系统,因为你添加了C#标签。
通常每个进程有1个堆。
所以,如果我们有一个包含5个线程的进程,是否正确地说我们将有5个堆栈和1个堆?
是的。每个线程立即消耗1MB的虚拟地址空间用于线程堆栈。
如果是这样,线程能够访问彼此的堆栈吗(或者这正是它们具有独立堆栈来防止损坏的原因),如果只有1个堆,那么显然它们都可以访问该堆,因此需要在多个线程中进行锁定?我理解得对吗?
是的,现代环境非常好地隔离了它们,因此您不能通过另一个线程直接访问其他线程堆栈。

Lukas,你看到Jakub-Konecki的回答了吗?它与“...因此您无法访问其他线程堆栈”有何关联?为什么会存在“锁定”机制? - Alex F
你肯定可以从另一个线程访问一个线程的堆栈,但最好避免这种直接通信,而是使用一个可从两个线程访问的对象队列。 - Martin James
1
吹毛求疵:一个进程有(正常的)托管堆、大对象堆,你可以把非托管堆看作是第三个。 - H H
@MartinJames - 访问另一个线程的堆栈确实不容易也不常见。或许可以使用指针。 - H H
1
@MartinJames 在本地代码中肯定没问题,在 .Net 中就比较复杂了。默认情况下是关闭的。使用 "unsafe" 关键字可以实现。 - Lukasz Madon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接