为什么线程之间要使用IPC机制?

6

我有一个疑问,线程在进程中共享所有段,但栈除外。因此,如果要在线程之间通信,比如我想从一个线程传递一个单词“你好”到另一个线程,是否需要使用IPC机制(例如消息队列)。


2
你是否意识到IPC代表“进程间通信”?要在线程之间进行通信,只需将单词放入内存对象中,这两个线程都知道即可。 - Todd Li
你需要IPC机制,因为内核也需要;如果你想要与内核通信,基本上你必须处理另一个线程/进程,这是IPC发挥作用的又一个例子。 - user2485710
1
你需要IPC机制来同步任务/进程/线程之间的操作。例如,你需要避免“竞争”条件,其中一个线程在另一个线程生成数据之前尝试读取数据。 - dar7yl
1
你不需要IPC机制来同步线程--线程同步和IPC是两个独立的概念。 - Stabledog
2个回答

14

在进程内或跨进程线程之间,有许多种数据通信方式可供选择,您只需选择适合您需求的一种即可。

共享内存

单个进程中的线程可以访问进程中的所有内存,尽管它们有自己的堆栈。这意味着一个线程可以很容易地与另一个线程共享一个内存片段。通常情况下,您会使用某种信号量来确保它们不会同时访问内存(否则通常会导致非常混乱的程序行为...)。

代码快速高效,因为数据从未被复制,但是对于编写复杂程序来说可能非常困难。

消息队列、管道等

然而,像消息队列这样的东西是从一个线程使用的内存发送数据到另一个线程使用的内存的一种方式。

使用内存队列编写复杂程序通常更加容易,所以它具有很多优点。但是,内存队列天生就不太有效率。这是因为操作系统中的消息队列实现必须复制数据(通常要复制两次)。

线程 vs 进程

采用共享内存的方法,将所有线程放在单个进程中是自然的,而使用消息队列则更自然地将线程放在单独的进程中。不过,这并不重要。大多数操作系统允许进程向其他进程公开一些内存(例如Linux上的/dev/shm),任何进程都可以获取命名信号量。

因此,在架构上,“所有线程在一个进程中”与“所有线程在自己的进程中”的问题并不重要。您可能需要考虑操作系统的线程与进程上下文切换时间,因为这也会影响效率。

可扩展性

如果您选择像管道这样的IPC机制,则有可扩展性的机会。特别是,管道的行为非常类似于套接字。因此,如果您的程序在计算机上运行得太慢,将管道转换为套接字并将线程放置在分布在两台或多台计算机上的进程中并不那么困难。因此,使用管道意味着将您的应用程序转换为多计算机分布式应用程序并不是很困难。
当然,套接字要慢得多,因此您必须考虑在决定使用套接字之前每秒需要发送多少数据从一个线程到另一个线程。
CPU内存体系结构
请记住,我说过存储器队列不如共享存储器效率高吗?嗯,这些日子里情况并不是很清楚。英特尔当前的架构,特别是AMD的架构,意味着复制数据与仅读取数据并没有太大区别。
看看Intel芯片之间的QPI链接,想想电子学级别实际发生了什么。假设您的计算机中有两个已填充的CPU插槽,并且您的应用程序具有共享内存缓冲区的两个线程。
对于一个位于一个芯片上的线程要访问驻留在另一个芯片的存储器中的数据,它必须沿着QPI链接读取该数据。
现在,想象一下那些线程改用了消息队列。操作系统必须将数据从一个线程复制到另一个线程。这意味着读取跨QPI链接的数据。在QPI链接上大约有相似数量的活动。
AMD的架构使这更加明显;他们等效(而且优越...)于QPI的Hypertransport,在AMD的芯片内部运行。
这不是那么直截了当。高速缓存会使事情变得更加复杂。但对于一些程序,其中有很多线程分布在两个或多个芯片上共享大量数据,则QPI有可能拖慢所有内容。英特尔当然知道这一点,并设计它,以便通常QPI不成为瓶颈。但如果您的程序不是英特尔在选择QPI参数时考虑的那种程序,则性能可能低于预期。

在某些情况下,通过使用管道、消息队列等方式,并进行大量细心的处理,可以实现比使用共享内存的程序性能更好的效果。

然而,出于简单性和可靠性方面的原因,我仍会选择使用管道和消息队列。


11

你应该花几个小时通过阅读一本好的Pthreads教程来学习更多内容,特别是基础的IPC(进程间通信)机制。你可以通过阅读高级Linux编程来更好地理解。

如果你的C++编译器支持最新的C++11标准,那么你可以使用其C++线程库(通常是在pthread之上构建的)。为此,你需要使用GCC 4.8或更高版本,或者Clang 3.3或更高版本(使用-std=c++11标志来编译g++clang++编译器)。

某个给定的进程的所有线程通过某种内存模型共享相同的地址空间(通常使用硬件辅助的缓存一致性)。

我建议您了解更多关于proc(5)的信息。命令cat /proc/1234/maps会显示pid为1234的进程的地址空间。

特别地,堆栈并不是每个线程的隔离段:换句话说,一个线程可以访问其他线程堆栈上的一些数据。例如,您可以在一个具有int x;局部变量的线程上,将地址&x;写入某个位置(例如,在全局变量g中使用g = &x; C语句),然后通过*g解引用该指针(但这是一种糟糕的编程风格)。给定进程的所有线程的堆栈仍然位于同一个地址空间中。

然而,你真正需要的是同步线程。 pthreads 的主要目的是提供几种同步手段(互斥锁条件变量屏障信号量等)。最近的语言标准也有原子性构造,例如C++11的std::atomic或C11的stdatomic.h。注意竞态条件死锁!调试多线程(或其他并行)程序很痛苦,因为可能会出现海森堡式错误。你可能应该倾向于函数式编程风格。

多线程编程很难,因为并行编程很困难。

您可以在线程之间使用IPC机制,例如,您可以有一个pipe(2),让一个线程写入它,另一个线程读取它(也许使用poll(2)进行多路复用...)。但是编写线程时,人们通常更喜欢使用pthread机制(例如,将一些链接列表与全局数据链接,并通过locking互斥量串行地访问它)。然后,您需要使用条件变量信号从空到非空的转换,并在从(空)链接列表中获取元素时等待该条件。这是一个producer-consumer情况。您可以有一个共享队列或列表等字符串,例如"Hello World",但仍然需要同步线程。


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