为什么线程共享堆空间?

30

每个线程都有自己的堆栈,但它们共用一个公共的堆。

众所周知,堆栈是用于本地/方法变量,堆则是用于实例/类变量。

共享堆对线程有何益处?

由于同时运行了多个线程,因此共享内存可能会导致并发修改、相互排除等问题,增加了负担。

在堆中,线程共享哪些内容。

为什么要这样做?为什么不让每个线程都拥有自己的堆?能否提供一个现实世界的例子来说明这一点,即如何利用线程之间的共享内存?


2
这个问题需要更多的澄清。例如,“实际想法”和“实时示例”是什么意思?此外,它听起来像是一道作业题...如果是的话,请标记它。 - jdmichal
1
我已经重写了它,但如果这不是问题的意图,请回滚或修改它。 - Jonathon Faust
不,不是所有人都清楚堆用于实例/类变量。还有其他有用的变量可以存储在堆上,在许多编程语言中,实例/类变量出现在堆栈上。 - Puppy
如果你不想共享堆,那么最好使用 fork() - Matt Joiner
8个回答

42

当您想要将数据从一个线程传递到另一个线程时,您会做什么?(如果您从未这样做,那么您将编写单独的程序而不是一个多线程程序。)有两种主要方法:

  • 您似乎默认采用的方法是共享内存:除了具有强制为特定线程保留的数据(例如堆栈)之外,所有数据都可供所有线程访问。基本上,有一个共享堆。这提供了速度:任何时候线程更改某些数据,其他线程都可以看到它。(限制:如果线程在不同的处理器上执行,则程序员需要非常努力地正确且高效地使用共享内存。)大多数主要的命令式语言,特别是Java和C#,支持此模型。

    也可以每个线程一个堆,再加上一个共享堆。这需要程序员决定将哪些数据放在何处,而这通常与现有的编程语言不太匹配。

  • 双重方法是消息传递:每个线程都有自己的数据空间;当一个线程想要与另一个线程通信时,它需要显式地向另一个线程发送消息,以将数据从发送者的堆复制到接收者的堆。在这种情况下,许多社区更喜欢将线程称为进程。这提供了安全性:由于线程不能随意覆盖其他线程的内存,因此可以避免很多错误。另一个好处是分布:您可以使线程在不更改程序中的任何一行的情况下在单独的机器上运行。您可以为大多数语言找到消息传递库,但集成通常不太好。理解消息传递的良好语言包括ErlangJoCaml

实际上,消息传递环境通常在幕后使用共享内存,至少在线程在同一台机器/处理器上运行时是这样。这可以节省大量的时间和内存,因为从一个线程传递消息到另一个线程不需要复制数据。但由于共享内存对程序员不可见,它固有的复杂性仅限于语言/库的实现。


1
非常好的答案。事实上,一些旧操作系统将系统中的所有程序基本上视为一个大型系统进程中的线程(我认为System/360就是这样做的?)。共享内存和消息传递之间的哲学差异是Windows和Unix之间设计差异的核心,即使在今天也是如此。 - Daniel Pryden
1
@Daniel:许多嵌入式系统仍然这样做,因为在以kB计算内存时强制执行进程分离是很昂贵的,并且它需要硬件支持(通常通过MMU实现)。我不明白Windows和Unix在并发处理方面有何不同,您能详细说明一下吗? - Gilles 'SO- stop being evil'
2
我的意思是,Windows平台更偏向于使用共享内存解决方案,并且具有操作系统级别的线程支持。另一方面,Unix传统上更喜欢通过管道和套接字进行通信,而不是共享内存解决方案。这绝不是一个硬性的区分,因为两种解决方案都可以在两个平台上使用,但每种方式都有其“首选”方式,这导致了我在评论中描述的“哲学差异”。 - Daniel Pryden

14

否则它们就会成为进程。 这就是线程的整个概念,共享内存。


4

进程通常不共享堆空间。虽然有API允许这样做,但默认情况下进程是分离的。

线程共享堆空间。

这就是“实际想法”——使用内存的两种方式——共享和不共享。


进程可以共享堆空间 - 共享内存API提供了这个功能。哦,还有Windows 3.1 -> Windows Me共享堆 :) - gbjbaanb
1
需要特殊的API才能完成 - 不是默认的。 - S.Lott
在Linux上,您可以使用clone()共享任何您喜欢的内容。 - Matt Joiner

2

在许多语言/运行时中,栈被用于保存函数/方法参数和变量等内容。如果线程共享一个栈,那么情况将会变得非常混乱。

void MyFunc(int a) // Stored on the stack
{
   int b; // Stored on the stack
}

当调用'MyFunc'时,栈被弹出,a和b不再在栈上。由于线程不共享堆栈,因此变量a和b没有线程问题。
由于栈的性质(推入/弹出),它并不适合在函数调用之间保持“长期”状态或共享状态。例如:
int globalValue; // stored on the heap

void Foo() 
{
   int b = globalValue; // Gets the current value of globalValue

   globalValue = 10;
}

void Bar() // Stored on the stack
{
   int b = globalValue; // Gets the current value of globalValue

   globalValue = 20;
}


void main()
{
   globalValue = 0;
   Foo();
   // globalValue is now 10
   Bar();
   // globalValue is now 20
}

1
堆是指动态分配的栈外所有内存。由于操作系统提供了单个地址空间,因此很明显堆是定义上进程中所有线程共享的。至于为什么栈不是共享的,那是因为执行线程必须有自己的栈来管理其调用树(例如,它包含有关离开函数时要执行的操作的信息!)。
现在,您当然可以编写一个内存管理器,根据调用线程从地址空间的不同区域分配数据,但其他线程仍然能够看到该数据(就像如果您以某种方式泄漏了指向线程栈上某个东西的指针给另一个线程,那么该线程可以读取它,尽管这是一个可怕的想法)。

严谨地说,许多内存管理器确实会从不同的区域(arena)分配内存,但这样做是为了提高性能。当然,结果的内存仍然是共享的。 - ninjalj

1
问题在于,使用本地堆栈会增加相当大的复杂性,而带来的价值却很小。
虽然有一些小的性能优势,但是 TLAB(线程本地分配缓冲区)可以很好地处理这个问题,并且可以透明地为您提供大部分优势。

1
在多线程应用程序中,每个线程都将有其自己的堆栈,但将共享相同的堆。这就是为什么在您的代码中应注意防止堆空间中的任何并发访问问题。堆栈是线程安全的(每个线程都将有其自己的堆栈),但堆不是线程安全的,除非通过您的代码进行同步保护。

0
那是因为线程的理念是“分享一切”。当然,有一些东西是无法共享的,比如处理器上下文和栈,但其他所有东西都是共享的。

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