8

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

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

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

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

当然,有一些明显的利弊:

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

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

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

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

3 回答 3

7

这是我能想到的:

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

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

听起来您正在实施通信顺序流程。出色的!

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

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

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

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

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

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

祝你好运!

于 2013-10-03T22:32:35.130 回答
0

您的问题缺少对“模块”如何实现以及它们的作用的描述,以及可能对您计划实现所有这些的环境的描述。

例如:

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

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

tl;博士:你的问题只是触及了表面:)

于 2013-10-04T10:57:27.310 回答