#actor-framework #actor #akka #erlang #elixir

maxim

基于 Erlang/Elixir 和 Akka 的最佳实践,为 Rust 实现了一个高度可扩展和易于使用的actor系统。Axiom actor框架的分支。

1 个不稳定版本

0.1.0-alpha.02020年3月27日

#892 in 并发

Apache-2.0

215KB
3.5K SLoC

Rust 的高度可扩展和易于使用的actor模型实现

Latest version Average time to resolve an issue License Changelog.md

Maxim

Maxim 是一个分支自 Axiom actor 框架,是为了在使用我们自己的 Actor 框架设计理念的同时,能够使用出色的 Actor 框架而创建的。

Maxim 基于在 Akka 和 Erlang 中多年 Actor 模型实现的经验教训,将高度可扩展的 actor 模型引入 Rust 语言。然而,Maxim 不是一个对上述两种 actor 模型的直接重实现,而是一个从这些项目的优点中汲取灵感的全新实现。

Maxim 的当前开发重点在于学习框架的工作原理,并实验我们的设计理念。我们将推出带有我们更改的 0.1.0-alpha 发布版本,直到达到相对可用的程度。从分支以来,我们首先增加的功能是一个允许您创建演员池的 spawn_pool 功能。随着我们在项目中测试这些功能,这些功能以及我们添加的其他功能可能会发生变化和适应。

我们正在考虑更改的其他事项包括

  • 使用 Agnostik 作为执行器,以允许 Maxim 在任何执行器上运行
    • 如果 Agnostik 无法满足某些原因,我们可能会切换到 Tokio 作为执行器,以避免维护自己的
  • 添加一个可选宏来匹配消息类型
  • 添加一个选项,使用有界或无界通道进行actor消息(有关更多信息,请参阅下面的“Maxim 设计原则”)
    • 这可能会涉及到使用 Flume 作为通道的后备

入门指南

actor模型是一种异步编程架构,其特点是所有处理活动都使用actor。

actor具有以下特性

  1. 只能通过消息与actor进行交互。
  2. 一个参与者一次只处理一条消息。
  3. 参与者只会处理一条消息一次。
  4. 参与者可以向任何其他参与者发送消息,而不需要了解该参与者的内部结构。
  5. 参与者只发送不可变数据作为消息,尽管它们可能有可变内部状态。
  6. 参与者不受位置限制;它们可以从集群中的任何地方接收消息。

需要注意的是,在Rust语言中,第五条规则不能由Rust强制执行,但这是一项重要的最佳实践,对于基于Maxim创建参与者的开发者来说非常重要。在Erlang和Elixir中,由于语言的结构,第五条规则不能被违反,但这也会导致性能限制。最好允许内部可变状态,并鼓励不发送可变消息的良好实践。

重要的是要理解,这些规则结合起来使得每个参与者都像程序使用它们时的内存空间中的微服务一样运行。由于参与者消息是不可变的,参与者可以安全、轻松地交换信息,而无需复制大型数据结构。

虽然按照参与者模型编程是一个相当复杂的过程,但您只需要几行代码就可以用Maxim开始。

use maxim::prelude::*;
use std::sync::Arc;
use std::time::Duration;

let system = ActorSystem::create(ActorSystemConfig::default().thread_pool_size(2));

let aid = system
    .spawn()
    .with(
        0 as usize,
        |state: usize, _context: Context, _message: Message| async move {
            Ok(Status::done(state))
        }
    )
    .unwrap();

aid.send(Message::new(11)).unwrap();

// It is worth noting that you probably wouldn't just unwrap in real code but deal with
// the result as a panic in Maxim will take down a dispatcher thread and potentially
// hang the system.

// This will wrap the value `17` in a Message for you!
aid.send_new(17).unwrap();

// We can also create and send separately using just `send`, not `send_new`.
let message = Message::new(19);
aid.send(message).unwrap();

// Another neat capability is to send a message after some time has elapsed.
aid.send_after(Message::new(7), Duration::from_millis(10)).unwrap();
aid.send_new_after(7, Duration::from_millis(10)).unwrap();

此代码创建了一个参与者系统,通过spawn()方法获取参与者的构建器,创建参与者,并最终向参与者发送消息。一旦参与者完成处理消息,它将返回参与者的新状态和处理此消息后的状态。在这种情况下,我们没有更改状态,所以我们只返回它。创建Maxim参与者实际上就这么简单,但还有更多功能可用。

请注意,如果您从环境中捕获变量,您必须将async move {}块包裹在另一个块中,然后将您的变量移动到第一个块中。请参阅测试用例以获取更多示例。

如果您想使用简单的结构创建参与者,也可以。让我们创建一个处理几种不同消息类型的参与者

use maxim::prelude::*;
use std::sync::Arc;

let system = ActorSystem::create(ActorSystemConfig::default().thread_pool_size(2));

struct Data {
    value: i32,
}

impl Data {
    fn handle_bool(mut self, message: bool) -> ActorResult<Self> {
        if message {
            self.value += 1;
        } else {
            self.value -= 1;
        }
        Ok(Status::done(self))
    }

    fn handle_i32(mut self, message: i32) -> ActorResult<Self> {
        self.value += message;
        Ok(Status::done(self))
    }

    async fn handle(mut self, _context: Context, message: Message) -> ActorResult<Self> {
        if let Some(msg) = message.content_as::<bool>() {
            self.handle_bool(*msg)
        } else if let Some(msg) = message.content_as::<i32>() {
            self.handle_i32(*msg)
        } else {
            panic!("Failed to dispatch properly");
        }
    }
}

let data = Data { value: 0 };
let aid = system.spawn().name("Fred").with(data, Data::handle).unwrap();

aid.send_new(11).unwrap();
aid.send_new(true).unwrap();
aid.send_new(false).unwrap();

此代码从一个任意的结构体中创建了一个命名的参与者。由于创建参与者唯一的要求是有一个符合maxim::actors::Processor特质的函数,所以任何东西都可以成为参与者。如果这个结构体被声明在您无法控制的地方,您可以使用它作为状态,通过声明自己的处理函数并调用第三方结构来使用它。

重要的是要记住,起始状态被移动到参与者中,之后您将无法访问它。这是设计使然,尽管您可以想象使用ArcMutex将结构作为状态封装,但这绝对是一个坏主意,因为它会破坏我们为参与者制定的规则。

详细示例

  • Hello World:任何计算机系统的必由之路。
  • Dining Philosophers:使用Maxim解决计算机科学中经典有限状态机问题的示例。
  • Monte Carlo:如何使用Maxim进行并行计算的示例。

Maxim的设计原则

这些都是Axiom项目的基本原则,Maxim是从这个项目分叉出来的。

  1. 在核心上,演员只是一个处理消息的函数。 最简单的演员是一个接受消息但简单地忽略它的函数。与Akka模型相比,函数式方法的好处是允许用户轻松简单地创建演员。这是微模块编程的概念;从最小的组件构建复杂系统的概念。基于演员模型的软件可能会变得复杂;保持核心简单是坚实架构的基本。
  2. 演员可以是有限状态机(FSM)。 演员通常按接收的顺序接收和处理消息。然而,在某些情况下,演员必须切换到另一个状态并处理其他消息,跳过某些消息稍后处理。
  3. 在跳过消息时,消息不能移动。 Akka允许通过将消息储藏到另一个数据结构中来跳过消息,然后稍后恢复这个储藏。这个过程有许多固有的缺陷。相反,Axiom允许演员在其通道中跳过消息,但将它们留在原处,从而提高性能并避免许多问题。
  4. 演员使用有界容量通道。 在Axiom中,演员通道的消息容量是有界的,这导致了更大的简洁性和对良好演员设计的重视。
  5. Axiom应该尽可能小。 Axiom是演员模型的核心,不应该扩展到包含演员可能需要的所有功能。那应该是扩展Axiom的库的工作。Axiom本身应该是微模块编程的例子。
  6. 测试是最好的例子。 Axiom的测试将非常全面和良好维护,应该为想要使用Axiom的人提供资源。它们不应该成为复制粘贴或废弃代码的垃圾场。最好的测试看起来就像架构化代码。
  7. 非常重视用户界面。 Axiom应该容易使用。

Maxim可能无法保留的原则是4和6号原则。为了解决这些问题

  • 有界容量通道:虽然最好有一个有界容量通道,但在我们形成自己的观点和初步反应之前,我们需要对这个设计进行一些实验。我们最初的反应是,应该允许用户选择。就复杂性而言,我们可能会考虑将通道实现外包给类似Flume的东西。尽管如此,还没有足够的调查来做出这样的陈述。
  • 测试是最好的例子:虽然我们同意测试应该是代码实际使用的例子,但我们不太可能建议用户去查看单元测试以了解如何使用库。我们希望文档丰富且对用户有帮助,这样他们就不需要必须查看测试来了解如何使用工具。

依赖关系

~5MB
~94K SLoC