软件模块通过消息传递进行通信的“多进程”与“单进程多线程”

Posted

技术标签:

【中文标题】软件模块通过消息传递进行通信的“多进程”与“单进程多线程”【英文标题】:"Multi-process" vs. "single-process multi-threading" for software modules communicating via messaging 【发布时间】:2013-10-10 00:25:20 【问题描述】:

我们需要构建一个软件框架(或中间件),以便在一台机器上运行的不同软件组件(或模块)之间实现消息传递。该框架将提供以下功能:

模块之间的通信是通过“消息传递”来实现的。 每个模块都有自己的消息队列和消息处理线程,它们将同步处理每条传入消息。

根据上述要求,以下哪种方法是正确的(及其推理)?:

    将模块实现为进程,并通过共享内存进行消息传递 将模块实现为单个进程中的线程,并通过将消息对象推送到目标模块的消息队列来进行消息传递。

当然,有一些明显的缺点和优点:

在 Option-2 中,如果一个模块导致分段错误,则进程(从而整个应用程序)将崩溃。而且一个模块可以直接访问/改变另一个模块的内存,这可能会导致难以调试的运行时错误。 但是使用 Option-1,您需要注意您需要通信的模块刚刚崩溃的状态。如果软件中有 N 个模块,则系统可能存在 2^N 多个存活/崩溃状态,这些状态会影响模块上运行的算法。 再次在选项 1 中,发送方不能假定接收方已收到消息,因为此时它可能已经崩溃。 (但系统可以提醒所有模块某个特定模块已崩溃;这样,发送方可以断定接收方将无法处理该消息,即使它已成功接收到该消息)

我赞成选项 2,但我不确定我的论点是否足够可靠。你有什么意见?

编辑:根据要求澄清,这里有更多规范细节:

这是一个将在 Linux 操作系统上运行的嵌入式应用程序。 很遗憾,我不能告诉你项目本身,但我可以说项目有多个组件,每个组件将由自己的团队(3-4人)开发,并决定这些组件/模块之间的通信是通过某种消息传递框架进行的。 C/C++ 将用作编程语言。 “模块接口 API”将自动提供给模块开发人员的是:(1) 消息/事件处理程序线程循环,(2) 同步消息队列,(3) 函数指针成员变量,您可以在其中可以设置你的消息处理函数。

【问题讨论】:

因此,您正在尝试根据通信级别要求设计模块结构。除了通信,这些模块还应该做什么? 是的,模块的结构将根据通信框架提供的内容来塑造。这些模块将完成从网络套接字通信到运行机器学习算法的任务。一个模块可以根据自己的需要运行多个线程。但是模块间的通信将通过消息以同步的方式完成。如果您需要更多说明,请告诉我.. 如果发生段错误/访问违规或任何导致流程崩溃的情况,主要应该关注公司的财务损失。业务问题是您编写代码的原因。在“纯”编程的基础上,您会假设您只是找到任何错误并修复它们。我会在多进程上使用共享内存,仅用于需要在进程之间加载和共享并直接读取的任何“大量”数据,理想情况下应该是只读的或通过数据库管理。 【参考方案1】:

这是我能想到的:

多进程(1) 与单进程、多线程(2):

分段错误的影响:(2)中,如果一个模块导致分段错误,整个应用程序崩溃。在(1)中,模块具有不同的内存区域,因此只有导致分段错误的模块才会崩溃。 消息传递保证:(2)中,您可以假设消息传递得到保证。在(1)中,接收模块可能会在接收之前或处理消息期间崩溃。 模块间共享内存:在(2)中,整个内存由所有模块共享,因此可以直接发送消息对象。在(1)中,您需要在模块之间使用“共享内存”。 消息实现:(2)中,可以在模块之间发送消息对象,在(1)中你需要使用任一网络套接字,存储在共享内存中的 unix 套接字、管道或消息对象。出于效率考虑,将消息对象存储在共享内存中似乎是最佳选择。 模块之间的指针使用:在(2)中,您可以在消息对象中使用指针。堆对象的所有权(通过消息中的指针访问)可以转移到接收模块。在(1)中,您需要手动管理“共享内存”区域中的内存(使用自定义 malloc/free 函数)。 模块管理:在(2)中,您只管理一个进程。在(1)中,您需要管理一个进程池,每个进程代表一个模块。

【讨论】:

【参考方案2】:

听起来您正在实施通信顺序流程。太棒了!

首先处理线程与进程,我会坚持使用线程;上下文切换时间更快(尤其是在进程上下文切换非常慢的 Windows 上)。

第二,共享内存与消息队列;如果您正在执行完全同步的消息传递,那么它对性能没有影响。共享内存方法涉及一个共享缓冲区,该缓冲区由发送者复制并由阅读器复制。这与消息队列所需的工作量相同。所以为了简单起见,我会坚持使用消息队列。

事实上,您可能想考虑使用管道而不是消息队列。您必须编写代码以使管道同步(它们通常是异步的,这将是 Actor 模型;消息队列通常可以设置为零长度,这可以实现您想要的同步和正确的 CSP),但是然后您可以很容易地使用套接字。如果需要,您的程序可以成为多机分布式,但您根本不需要更改架构。进程之间的命名管道也是一个等效选项,因此在进程上下文切换时间良好的平台(例如 linux)上,整个线程与进程的问题都消失了。因此,更加努力地使用管道可以为您提供非常重要的可扩展性选项。

关于崩溃;如果你走多进程路线,并且希望能够优雅地处理进程失败,你将不得不做一些工作。本质上,您将需要在消息传递通道的每一端都有一个线程来监视另一端的响应能力(可能通过在它们之间来回弹跳保持唤醒消息)。这些线程需要将状态信息提供给它们相应的主线程,以告知它另一端何时未能按时发送保持唤醒。然后主线程可以相应地采取行动。当我这样做时,我让监控线程在可能的时候自动重新连接(例如,远程进程恢复了生命),并告诉主线程。这意味着我的系统的某些部分可以来来去去,而其余部分则可以很好地应对。

最后,您的实际应用程序进程将最终形成一个循环,顶部有类似 select() 的东西,以等待来自它期望听到的所有不同通道(和监控线程)的消息输入。

顺便说一句,这种事情在 Windows 中很难实现。在任何 Microsoft 语言中都没有适当的 select() 等价物。有一个用于套接字的 select(),但你不能像在 Unix 中那样在管道等上使用它。 Cygwin 的家伙在实现他们的 select() 版本时遇到了真正的问题。我认为他们最终为每个文件描述符提供了一个轮询线程;非常低效。

祝你好运!

【讨论】:

您说“共享内存方法涉及一个共享缓冲区,该缓冲区由发送方复制并由读取方复制”,但消息的内容可以是指向另一个内存位置的指针共享内存。 @BenjiMizrahi,抱歉回复太慢了 - 6 年。是的,您确实可以这样做,但是您必须解决所有权控制问题,以及允许谁修改共享内存。通过发送整个副本 - 通过管道或消息队列,您无需担心这一点。如果发件人修改了他们的原始数据并且收件人需要知道它,则必须发送另一个副本(或增量)。是的,这可能是低效的。自从我写了这个答案以来,ZeroMQ 已经走上了一条漫长的道路,如果可能的话,这就是我今天要使用的,【参考方案3】:

您的问题没有描述“模块”是如何实现的以及它们做了​​什么,并且可能没有描述您计划在其中实现所有这些的环境。

例如:

如果模块本身有一些要求使其难以实现为线程(例如,它们使用非线程安全的第 3 方库、具有全局变量等),您的消息传递系统也将无法通过线程实现. 如果您使用的环境(例如 Python)不能很好地处理线程并行性(由于其全局解释器锁),并且在 Linux 上运行,则线程相对于进程不会获得任何性能优势。

还有更多的事情需要考虑。如果您只是在模块之间传递数据,谁说您的系统需要使用多个线程或多个进程?还有其他架构在没有其中任何一个的情况下执行相同的操作,例如带有回调的事件驱动(消息接收器可以向您的系统注册回调,当消息生成器生成消息时调用该回调)。在并行性不重要并且可以在调用者的执行上下文中调用接收代码的任何情况下,这种方法绝对是最快的。

tl;dr: 你的问题只是触及了皮毛:)

【讨论】:

在收到您的反馈后,我对我的问题做了一些澄清。模块接口 API 将提供的正是您提到的:带有回调的事件驱动消息传递。我的问题是在单进程多线程(每个线程是这个事件处理机制的主循环)还是在多进程(每个进程的主线程负责事件处理)的方式下使用它?跨度> 如果您在进程之间进行通信,您是否考虑过使用共享内存以外的东西,例如管道和插座?这样,您将获得接收器是否崩溃的更简单(仍然不是微不足道的)指示:如果发送者尝试再次发送,则会收到错误消息。由于您不信任您的模块(您担心线程会破坏彼此的内存 - 您的优点与缺点#3),因此多进程方法是唯一可以解决此问题的方法。 如果我使用套接字或管道,我将需要序列化/反序列化消息对象。使用共享内存,我可以将消息对象存储在共享内存中,并传递一个指向消息对象的指针(这些指针可以通过管道或套接字发送) 如果你有它在共享内存中,这意味着要么a)它是一个连续的数据块,一个没有指针的结构,要么b)你依赖于共享内存块将在所有进程中的地址完全相同(如果您在设置共享内存后将进程从主进程分叉,则可能是这样)。如果 a) 情况为真,那么您不需要任何特殊的序列化,您可以简单地发送原始数据。这只是一个想法;我认为你已经涵盖了所有方面:如果你需要性能,使用线程,如果你需要隔离(例如为了安全),使用进程。

以上是关于软件模块通过消息传递进行通信的“多进程”与“单进程多线程”的主要内容,如果未能解决你的问题,请参考以下文章

node 多进程学习

node 多进程学习

Python进阶第二篇多线程消息队列queue

Linux多进程的应用

进程间通信(IPC)——Unix域套接字 VS 网络套接字

多进程和多线程的架构的简单介绍