1个不稳定版本

0.1.0 2024年7月8日

#95 in 模拟

每月 49 次下载
anysystem 中使用

MIT/Apache

165KB
2K SLoC

SimCore

SimCore是一个离散事件模拟框架,旨在为构建分布式和其他类型系统的模拟模型提供一个坚实的基础。该框架围绕一个通用的基于事件驱动的编程模型构建,可用于模拟不同的领域。它允许使用回调和异步等待方便地模拟任何执行逻辑。

你可能还在寻找

许可

SimCore采用Apache-2.0许可证MIT许可证进行许可,您可自行选择。

除非您明确说明,否则您根据Apache-2.0许可证定义的任何旨在包含在SimCore中的有意提交的贡献,将根据上述方式双许可,而无需任何额外的条款或条件。


lib.rs:

SimCore是一个离散事件模拟框架,旨在为构建分布式和其他类型系统的模拟模型提供一个坚实的基础。该框架围绕一个通用的基于事件驱动的编程模型构建,可用于模拟不同的领域。它允许使用回调和异步等待方便地模拟任何执行逻辑。

内容

基本概念

SimCore支持编写任意的模拟模型,这些模型由用户定义的组件组成,这些组件发出和处理事件

组件。 组件代表模型的一部分,具有一些内部状态和执行逻辑。每个组件都分配了一个唯一的标识符,可用于为该组件发出事件。组件可以通过框架提供的上下文访问模拟状态、发出或等待事件。模型执行由框架驱动,框架在事件发生时调用组件。被调用的组件可以检查接收到的事件、读取当前模拟时间、修改内部状态并根据用户编写的代码发出新事件。组件通过创建命名上下文并注册事件处理器来添加。前者对于发出事件的组件是必需的,而后者对于处理事件的组件是必需的。

事件。 事件包含时间戳、事件源和目标标识符以及用户定义的有效负载。事件时间戳对应于事件应发生时的模拟时间。在事件发出时必须指定此时间。由于性能原因,事件时间戳不能更改。但是,在事件发生之前可以取消事件,并通过创建新的事件重新安排。每个事件都与一个目标组件关联。框架允许通过使用任意数据结构作为事件有效负载来模拟不同类型的事件。事件有效负载对框架是透明的,并且在事件源和目标之间以零拷贝的方式传递。

在模拟开始之前,通过某些组件创建初始事件集。例如,在跟踪驱动的模拟中,可以使用专用组件作为来自跟踪的外部事件源。

模拟。 遵循离散事件模拟方法,通过处理模型组件发出的连续事件序列来实现用户定义模型的执行。框架按时间戳顺序处理事件,通过将模拟时钟推进到事件的时戳并调用指定为事件目标组件来处理事件。在处理事件时,组件可以通过其上下文创建和发出具有任意未来时间戳的新事件。还可以在处理之前取消先前发出的事件。

选择描述的模拟模型构建方法基于以下考虑。

首先,它非常适合模拟分布式系统。实际上,这类系统通常被建模为一系列进程,它们通过网络相互发送消息进行通信。在这些模型中,事件可以是内部进程事件或从其他进程接收的消息。描述的方法允许模拟这两种类型的事件,并且非常适合消息传递 - 可以通过向对应组件发出一个延迟等于消息传输时间的事件来向另一个进程发送消息。

其次,描述的模型足够抽象和灵活,可以支持不同的模拟需求,甚至超出了分布式系统。如果框架基于更具体和受限的模型,例如消息传递,则会复杂化其他活动的建模,例如计算。一些框架选择采用的方法是提供一组预定义的内置活动和事件。然而,这将在建模特定类型系统的易用性和框架的灵活性之间引入权衡。

我们通过尽可能保持框架的通用性,并在其之上构建具有特定领域模型的独立库来克服这种权衡。这使得用户可以选择他们需要的功能,并在某些功能缺失时创建新的库,而不使框架臃肿。例如,SimCore中没有进程、主机和网络的概念。这样的抽象及其模型可以通过单独的库添加。根据目的,一些用户可能需要一个复杂网络模型,而其他人则只需要框架支持的固定延迟。

示例

此示例演示了SimCore编程接口的使用以及通过回调接收事件。有关详细信息和使用基于回调方法的替代方法的说明,请参阅下一节。

编程接口

Simulation 是框架的主要接口,允许配置和执行仿真模型。如上例所示,它可以使用用户定义的随机种子进行实例化,然后用于创建仿真上下文和注册用户定义类型 Process 的组件事件处理器,运行仿真并获取当前仿真时间。除了 step_until_no_events 方法外,它还提供了其他精确步骤通过仿真的方法。它还提供了对使用用户定义种子初始化的全局随机数生成器的访问,以支持确定性的仿真。

SimulationContext 是访问仿真状态和从组件中发出事件的接口。每个组件都与一个唯一命名的上下文相关联,该上下文通过 Simulation::create_context 方法创建。上下文通常传递给组件的构造函数,并如上例所示存储在组件内部。此示例还说明了使用存储的上下文来发出用户定义的事件 RequestResponse,获取当前仿真时间,并使用仿真全局生成器生成随机数。

SimCore 允许用户保持对组件的引用以直接调用它,如上例中的 proc1_ref 所示。将组件完全移动到框架内部并仅通过事件或框架接口与它们交互会损害可用性。通过发出特殊事件来调用 proc1 而不是调用 send_request 方法会更繁琐。这也允许在仿真期间轻松检查组件状态。

对组件之间的交互也有同样的观察结果——如果假设立即请求/响应,则通过直接调用而不是事件进行交互既方便又高效。例如,一个模拟 CPU 的组件可以直接由在同一模拟机器上运行的另一个组件调用以请求计算。作为回应,CPU 组件可以返回请求句柄并通过事件通知请求者在计算完成后。因此,框架不限制组件之间以及与组件的交互仅通过事件进行。这与类似但更严格的模型(如消息传递的演员模型)形成对比。

所描述的接口仅处理从用户代码调用 SimCore。然而,框架还应该能够调用用户组件以通知它们发生的事件。下面描述了两种支持编程此逻辑的方法。

通过回调接收事件

在组件中接收事件的默认方法是基于实现 EventHandler 接口。此接口包含一个单一的 on 方法,框架通过此方法调用以将事件传递给目标组件。如上例所示,这种方法通过 Process 组件实现此接口来接收 RequestResponse 事件进行了说明。使用模式匹配语法来识别接收事件的类型。当组件实现 EventHandler 接口时,必须通过 Simulation::add_handler 方法在框架中注册。

详细考虑所提供的示例。它描述了一个由两个组件组成 proc1proc2 构成的仿真模型。这些组件的行为由 Process 类型定义。此类型实现了 EventHandler 接口,以接收和处理两种类型的事件:RequestResponse

  • 处理 Request 的逻辑定义在 on_request 回调方法中——进程以一定延迟(包括随机请求处理时间和网络延迟)向 Request 的源发射 Response。存储在 Request 中的请求发送时间被复制到相应的 Response

  • 处理 Response 的逻辑定义在 on_response 回调方法中——进程从 Response 中读取请求时间来计算和打印响应时间,即请求发送和接收响应之间的时间差。

进程实现还包括 send_request 方法,用于触发向另一个进程发射 Request

该示例模拟了一个简单场景,其中 proc1proc2 发射请求,仿真运行直到 proc1 收到响应。

回调的限制

虽然基于回调的方法通过将所有事件处理逻辑组织在 EventHandler 中而简单直观,但它也可能使组件内部更复杂的逻辑实现复杂化。特别是当模拟某些多步骤活动时,每个步骤都需要等待某些事件,这些步骤应该分布在几个事件处理函数中。这使得这些复杂活动的实现更冗长,更难以理解。

例如,在提供的示例中,请求的发送和响应的接收被分为两个单独的方法,而代码中在发送请求后立即等待响应事件会更方便。这也使得响应时间的计算变得复杂,因为在 on_response 回调方法中执行它时,请求发送时间应该包含在事件中或存储在进程内。

此外,随机处理时间在 on_request 中通过简单地将它加到响应事件延迟中来模拟,而代码中在发射响应之前“sleep”这段时间会更自然。当处理时间未知时,这个延迟技巧也无法工作。例如,请求的处理可能包括一些计算,其完成由一个单独的模型确定,并通过事件通知进程。在这种情况下,请求处理逻辑也应该分成几个方法,使其更难理解。

异步模式

为了克服基于回调的方法的描述限制,SimCore接口已经增加了用于生成异步活动和等待事件和计时器的原语。这个功能被称为“异步模式”,它作为一个可选功能实现,用户可以选择启用并与其结合使用。

下面的代码展示了如何使用异步模式来改进之前描述的基于回调的实现。

首先,现在将发送请求和接收响应的操作方便地合并到了单个 send_request_and_get_response 方法中。此方法代表由 send_request 通过 SimulationContext::spawn 触发产生的异步活动。在此活动内部等待响应事件是通过 SimulationContext::recv_event 方法实现的,该方法返回一个可以挂起的未来,而不会阻塞仿真。将请求-响应逻辑集中在一个方法中,可以在不传递请求时间到事件的情况下计算响应时间。

其次,现在在 process_request 方法中模拟请求处理,该方法代表在接收到请求后触发的异步活动。通过调用 SimulationContext::sleep 方法在 process_request 中模拟随机请求处理时间,该方法允许组件执行在指定时间内暂停。

配置和运行仿真代码略有变化。为了能够产生异步活动,组件必须实现特殊的 StaticEventHandler 特性,并使用 Simulation::add_static_handler 方法注册其实例。

如所示,异步模式消除了基于回调方法所述的限制。此示例还说明 SimCore 允许同时使用两种方法以结合它们的优势。虽然回调对于描述简单的事件处理逻辑或接收触发复杂逻辑的事件来说很方便,但后者可以使用异步模式原语方便地描述。

异步模式的另一个显著特性是支持通过用户定义的键选择性地接收事件(参见 SimulationContext::recv_event_by_key)。在组件执行多个异步活动,且每个活动必须等待相同类型的事件的情况下,这很方便。也可以使用 joinselect 原语从 futures crate 同时等待多个事件。

然而,与回调相比,异步模式具有额外的性能开销。根据我们的经验,观察到的减速取决于应用程序,大约在 10-50% 之间。

依赖项

~3–14MB
~119K SLoC