两个或多个线程如何共享它们所分配的堆内存?

19
如标题所示,两个或多个线程如何共享它们已分配的堆内存?我一直在思考但无法理解两个线程如何实现此操作。以下是我对该过程的理解,可能有错误。
任何线程都可以通过调用系统调用来添加或删除堆上的一定数量的字节,并返回指向此数据的指针,这可能通过向线程可以将其复制到堆栈中的寄存器中写入实现。 因此,线程A和B可以随意分配任意数量的内存。但我不知道线程A如何知道线程B分配的内存位置。也不知道任何一个线程如何知道另一个线程的堆栈位置。多线程程序共享堆,我相信可以访问彼此的堆栈,但我无法理解如何实现这一点。
我尝试搜索这个问题,但只找到抽象掉细节的特定于语言的版本。
编辑:我试图不具体指出语言或操作系统,但我正在使用Linux并从低级别的角度(汇编)查看它。

可能是 Do threads share the heap? 的重复问题。 - Tomas Voracek
2
不,我不这么认为。我在搜索时看到过那个问题,但它并没有问线程如何共享堆,只是询问它们是否这样做。我想要知道线程如何精确地共享数据。通信机制是什么?我认为它们共享指向分配内存的指针,但我不知道它们是如何实现的。 - Lemma Prism
我该如何编辑我的问题以获得最佳的清晰度?它最令人困惑的是什么?用户已经回答了我的问题,但我想确保我的问题对其他人来说是可理解的,而现在它似乎并不是很清晰。 - Lemma Prism
5个回答

13

我的理解是:线程A如何知道指向内存B的指针?他们如何交换数据?

答案:它们通常从一个共同的指针开始,指向一个共享内存区域。这使它们可以相互交换其他数据(包括指向其他数据的指针)。

例如:

  1. 主线程分配一些共享内存并将其位置存储在p
  2. 主线程启动两个工作线程,并将指针p传递给它们
  3. 工作线程现在可以使用p并处理由p指向的数据

在一个真正的语言(C#)里看起来像这样:

//start function ThreadProc and pass someData to it
new Thread(ThreadProc).Start(someData)

线程通常不会访问彼此的堆栈。一切都始于传递给线程过程的一个指针。


创建线程是操作系统的函数。它的工作方式如下:

  1. 应用程序使用标准ABI/API调用操作系统
  2. 操作系统分配堆栈内存和内部数据结构
  3. 操作系统“伪造”第一个堆栈帧:它将指令指针设置为ThreadProc,并将someData“推入”堆栈。我说“伪造”,因为这个第一个堆栈帧并不是自然产生的,而是由操作系统人为地创建的。
  4. 操作系统安排线程。ThreadProc不知道它已经在一个新的堆栈上设置。它知道的只有someData位于它所期望的通常堆栈位置。

这就是someData到达ThreadProc的方式。这是共享第一个初始数据项的方法。步骤1-3由父线程同步执行。第四步在子线程上执行。


主线程如何将指针p传递给工作线程?工作线程是主线程的副本吗?我读到过这就是新进程创建的方式,如果是这样,那肯定可以解释数据如何传递,但我不知道它们如何区分。我更多地从汇编的角度来看待这个问题,而不是从高级语言的角度。 - Lemma Prism
我已回复了你的评论。 - usr
好的,多线程通信的机制是通过在线程创建时将预定义数据(通常是指针)通过操作系统函数参数推送到线程堆栈上来启动的。谢谢!这回答了我的问题,但我会等一两天再接受答案,以防有人写出更好或更清晰的回答。这对我的目的已经足够了,但出于好奇,您能否给操作系统函数提供更多要推送到堆栈上的数据?此外,这是一个特定于操作系统的机制,还是Windows或Linux有不同的做法? - Lemma Prism
这在理论上是特定于操作系统的,但实际上这是唯一的方法。 一个指针总是足够的,因为你可以让该指针指向一个包含所有内容的任意结构体。线程池线程可能会收到指向(同步的)工作队列的指针。用户模式开发人员可以在这个简单的API之上放置任意的包装器。C#甚至允许你隐式地传递一个闭包对象。这是一个相当强大的包装器,隐藏了我刚才描述的所有东西。例如:{ int x = 0; new Thread(() => { x++; }); }只需要在这个简单的API之上构建就可以了。 - usr
一个指针足够了,但是你能不能添加更多的数据呢?我只是好奇操作系统是否允许这样做。实际上,无论如何都没有关系,因为像你说的那样,一个指针已经足够了。 - Lemma Prism
不可以。请参考 http://msdn.microsoft.com/en-us/library/bb202727.aspx (lpvThreadParam); http://linux.die.net/man/3/pthread_create (arg)。 - usr

2

鸟瞰下的简短回答(从1000英里高空看):
线程是同一进程的执行路径,堆实际上属于该进程(因此由线程共享)。每个线程只需要自己的堆栈来作为一个独立的工作单元。


0

线程与同一进程中的其他线程共享:其代码段、数据段以及其他操作系统资源,如打开的文件和信号。


0

如果线程都使用相同的堆,则线程可以在堆上共享内存。默认情况下,大多数语言/框架都有一个单一的默认堆,代码可以从中分配内存。在非托管语言中,通常需要显式调用以分配堆内存。例如,在C语言中,可能是malloc等。在托管语言中,堆分配通常是自动的,如何进行分配取决于语言 - 通常通过使用new运算符。但是,这略微取决于上下文。如果您提供要求的操作系统或语言上下文,我可能能够提供更多详细信息。


0
你所缺失的部分是包含静态变量的静态内存。
当程序启动时,这块内存被分配,并且被赋予已知地址(在链接时间决定)。所有线程都可以访问这块内存,而不需要在运行时交换任何数据,因为地址实际上是硬编码的。
一个简单的例子可能是这样的:
// Global variable.
std::atomic<int> common_var;

void thread1() {
  common_var = compute_some_value();
}

void thread2() {
  do_something();
  int current_value = common_var;
  do_more();
}

当然,全局值可能是一个指针,可以用来交换堆内存。生产者分配一些对象,消费者获取并使用它们。

// Global variable.
std::atomic<bool> produced;
SomeData* data_pointer;

void producer_thread() {
  while (true) {
    if (!produced) {
      SomeData* new_data = new SomeData();
      data_pointer = new_data;
      // Let the other thread know there is something to read.
      produced = true;
    }
  }
}

void consumer_thread() {
  while (true) {
    if (produced) {
      SomeData* my_data = data_pointer;
      data_pointer = nullptr;
      // Let the other thread know we took the data.
      produced = false;
      do_something_with(my_data);
      delete my_data;
    }
  }
}

请注意:这些不是良好并发代码的示例,但它们展示了一般思路而没有太多杂乱。

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