10 个版本
0.2.1 | 2019 年 12 月 19 日 |
---|---|
0.2.0 | 2019 年 12 月 6 日 |
0.1.0 | 2019 年 9 月 28 日 |
0.0.7 | 2019 年 8 月 19 日 |
0.0.5 | 2019 年 7 月 24 日 |
#561 in 异步
205KB
3K SLoC
Rust 的一个高度可扩展且易用的actor模型实现
Axiom
Axiom 将高度可扩展的actor模型引入 Rust 语言,基于 Akka 和 Erlang 多年 Actor 模型实现的经验教训。然而,Axiom 并不是上述两种actor模型的直接重实现,而是一个新的实现,从这些项目的优点中汲取灵感。
- 2019-12-19 0.2.1
- 修复了早期丢弃挂起的 Actor Handle 的关键问题。
- 修复了在 Actor Handle 的轮询中未捕获 panic 的关键问题。
- 2019-12-06 0.2.0
- 进行了一次大规模的内部重构,以支持异步 Actor。只有少数破坏性更改,因此移植到这个版本将相对简单。
- 破坏性更改:Processor 的签名已从
Context
和Message
的引用更改为值。对于作为 Actor 的闭包,请将主体包裹在async
块中。move |...| {...}
变为|...| async move { ... }
。对于常规函数语法,只需在fn
前面添加async
。 - 注意:根据语义,
move
的位置可能需要不同。值不能从闭包移动到异步块中。 - 重大变更:由于期货的特性,无法给演员的处理器提供一个可变引用到演员的状态。状态至少需要存在与期货一样长的时间,我们的研究未能找到一种简单的方法来做这件事。因此,现在当演员返回状态时,它也会返回新的状态。更多信息请参阅示例。处理器签名的现在如下
impl<F, S, R> Processor<S, R> for F where S: Send + Sync, R: Future<Output = AxiomResult<S>> + Send + 'static, F: (FnMut(S, Context, Message) -> R) + Send + Sync + 'static {}
- 重大变更:演员现在具有恐慌容忍性!这意味着
assert
和panic
将被捕获并转换,与错误处理相同。错误应该已经被认为是致命的,因为演员应该在其自己的范围内处理任何错误。 - 重大变更:错误类型已经被细分为更具体的上下文。
- 重大变更:在
ActorSystemConfig
结构体中添加了start_on_launch
标志。这使得可以在不立即启动的情况下创建 ActorSystem。有关如何启动未启动的ActorSystem
,请参阅ActorSystem::start
。 - 在
Status
中添加了辅助方法,以帮助演员的返回点。每个变体都有一个对应的函数,该函数接收演员的状态。Ok(Status::Done)
现在是Ok(Status::done(state))
。 - 用户应该注意,在运行时,演员将遵循 Rust 期货的语义。这意味着演员在等待期货时将不会处理任何消息,也不会继续执行,直到该期货准备好再次轮询。虽然 async/await 将提供异步 API 的便捷使用,但这可能是一个问题,并可能影响时间。
- 引入了一个序言。将尽力保持序言在大版本之间相对一致,并建议尽可能使用它。
- 在代码库中添加了更多的
log
点。
入门指南
演员模型是一种以演员为所有处理活动的特征的异步编程架构。
演员有以下特点
- 只能通过消息与演员交互。
- 演员一次只处理一条消息。
- 演员只会处理一条消息。
- 演员可以向任何其他演员发送消息,而无需了解该演员的内部结构。
- 演员只发送不可变数据作为消息,尽管它们可能有可变内部状态。
- 演员的位置无关;可以从集群中的任何地方发送消息给它们。
请注意,在 Rust 的语言中,第五条规则不能由 Rust 强制执行,但这是基于 Axiom 创建演员的开发人员的重要最佳实践。在 Erlang 和 Elixir 中,由于语言的架构,第五条规则不能被违反,但这也会导致性能限制。最好允许内部可变状态,并鼓励不发送可变消息的良好实践。
重要的是要理解,这些规则的组合使得每个演员都像程序内存空间中的微服务一样运行。由于演员消息是不可变的,演员可以安全、轻松地交换信息,而无需复制大型数据结构。
尽管在演员模型中进行编程是一个相当复杂的过程,但您只需用几行代码就可以开始使用 Axiom。
use axiom::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 Axiom 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();
此代码创建了一个actor系统,通过spawn()
方法获取actor构建器,创建actor并发送消息给actor。一旦actor处理完一条消息,它将返回actor的新状态和处理该消息后的状态。在这种情况下,我们没有改变状态,所以我们只返回它。创建Axiom actor实际上非常简单,但还有更多功能可供使用。
请注意,如果您需要从环境中捕获变量,您必须将async move {}
块包裹在另一个块中,然后将您的变量移动到第一个块中。请参阅测试用例以获取更多此类示例。
如果您想创建一个结构简单的actor,也可以。让我们创建一个处理几种不同消息类型的actor
use axiom::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();
此代码将任意结构创建为一个命名actor。由于创建actor的唯一要求是有一个符合axiom::actors::Processor
特质的函数,因此任何东西都可以成为actor。如果此结构在您无法控制的地方声明,则可以使用它作为actor中的状态,通过声明自己的处理函数并调用第三方结构来实现。
重要的是要记住,起始状态被移动到actor中,之后您将无法访问外部。 这是由设计决定的,尽管您可以使用Arc
或Mutex
来封装结构作为状态,但这绝对是一个糟糕的想法,因为它会破坏我们为actor制定的规则。
还有更多要学习和探索的内容,您最好的资源是Axiom的测试代码。开发者们相信,测试代码应该具有良好的架构和注释,以便作为Axiom用户的示例集。
详细示例
- Hello World:任何计算机系统的必经之路。
- Dining Philosophers:使用Axiom解决计算机科学中经典有限状态机问题的示例。
- Monte Carlo:如何使用Axiom进行并行计算的示例。
Axiom的设计原则
基于对其他actor模型的先前经验,我希望围绕一些核心原则设计Axiom
- 在核心上,actor只是一个处理消息的函数。 最简单的actor是一个接收消息并简单地忽略它的函数。与Akka模型相比,函数式方法的好处是允许用户轻松简单地创建actor。这是微模块编程的概念;从最小组件构建复杂系统的概念。基于actor模型的软件可能会变得复杂;保持核心简单是坚实架构的基本。
- 参与者可以是一个有限状态机(FSM)。 参与者按照接收的顺序通常接收和处理消息。然而,在某些情况下,参与者需要转换到另一个状态并处理其他消息,跳过某些消息稍后处理。
- 在跳过消息时,消息必须不能移动。 Akka 允许通过将消息存档到另一个数据结构来跳过消息,然后在稍后恢复存档。这个过程有很多固有的缺陷。相反,Axiom 允许参与者在其通道中跳过消息,但将它们留在原处,提高性能并避免许多问题。
- 参与者使用有界容量通道。 在 Axiom 中,参与者的通道消息容量是有界的,这导致了更大的简洁性和对良好参与者设计的重视。
- Axiom 应尽可能保持小巧。 Axiom 是参与者模型的核心,不应扩展以包含参与者可能需要的所有功能。那应该是扩展 Axiom 的库的任务。Axiom 本身应该是微模块编程的一个例子。
- 测试是最好的示例地方。 Axiom 的测试将非常广泛且维护良好,应该为希望使用 Axiom 的人提供资源。它们不应成为复制粘贴或废弃代码的垃圾场。最好的测试将看起来像架构化的代码。
- 非常重视 crate 用户的易用性。 Axiom 应该易于使用。
依赖项
~5MB
~94K SLoC