You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
170 lines
9.4 KiB
170 lines
9.4 KiB
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
|
<meta http-equiv="content-type" content="text/html; charset=utf-8"> |
|
|
|
<!-- Enable responsiveness on mobile devices--> |
|
<!-- viewport-fit=cover is to support iPhone X rounded corners and notch in landscape--> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, viewport-fit=cover"> |
|
|
|
<title>Julio Biason .Me 4.3</title> |
|
|
|
<!-- CSS --> |
|
<link rel="stylesheet" href="https://blog.juliobiason.me/print.css" media="print"> |
|
<link rel="stylesheet" href="https://blog.juliobiason.me/poole.css"> |
|
<link rel="stylesheet" href="https://blog.juliobiason.me/hyde.css"> |
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=PT+Sans:400,400italic,700|Abril+Fatface"> |
|
|
|
|
|
|
|
|
|
|
|
</head> |
|
|
|
<body class=" "> |
|
|
|
<div class="sidebar"> |
|
<div class="container sidebar-sticky"> |
|
<div class="sidebar-about"> |
|
|
|
<a href="https://blog.juliobiason.me"><h1>Julio Biason .Me 4.3</h1></a> |
|
|
|
<p class="lead">Old school dev living in a 2.0 dev world</p> |
|
|
|
|
|
</div> |
|
|
|
<ul class="sidebar-nav"> |
|
|
|
|
|
<li class="sidebar-nav-item"><a href="/">English</a></li> |
|
|
|
<li class="sidebar-nav-item"><a href="/pt">Português</a></li> |
|
|
|
<li class="sidebar-nav-item"><a href="/tags">Tags (EN)</a></li> |
|
|
|
<li class="sidebar-nav-item"><a href="/pt/tags">Tags (PT)</a></li> |
|
|
|
|
|
</ul> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="content container"> |
|
|
|
<div class="post"> |
|
<h1 class="post-title">Thinking About Rust Actors</h1> |
|
<span class="post-date"> |
|
2023-08-11 |
|
|
|
<a href="https://blog.juliobiason.me/tags/rust/">#rust</a> |
|
|
|
<a href="https://blog.juliobiason.me/tags/actor-model/">#actor model</a> |
|
|
|
</span> |
|
<p>I recently wrote an application for work (so, sorry, can't show you the code) |
|
that, 'cause it was heavily I/O based, I decided to write it using |
|
<a href="https://tokio.rs/">Tokio</a> and the idea of <a href="https://ryhl.io/blog/actors-with-tokio/">Actor Model with |
|
it</a>.</p> |
|
<p>... which gave me some things to think about.</p> |
|
<span id="continue-reading"></span> |
|
<p>Before anything, actors in Rust are very different from the actors in languages |
|
with the actual Actor Model. In summary, you have your actors, which running |
|
independently, each actor have an Inbox for things to be processed and an |
|
"outbox" -- in quotes, 'cause that's not really it. An actor can receive a |
|
message, process it and then it can just be done with it or it can produce |
|
something that it is send to another actor -- that's its outbox, which usually |
|
differs from the Inbox 'cause the Inbox need to have a queue of sorts, but the |
|
Outbox doesn't (and that's why I've been using "outbox" with quotes before).</p> |
|
<p>All the messages are delivered by a "post office" of sorts, that connects all |
|
Actors:</p> |
|
<p><img src="https://blog.juliobiason.me/code/thinking-about-rust-actors/actors.png" alt="" title="A silly representation of the actor model" /></p> |
|
<p>On my implementation, the actor is actually a module with a <code>run()</code> function; |
|
this function exposes the <code>Sender</code> part of a MPSC |
|
(Multiple-Producer-Single-Consumer) channel which acts as the Inbox of it and |
|
the task PID, so the can <code>.await</code> the actor processing loop to avoid the main |
|
application from finishing with the actor still running. </p> |
|
<div style="border:1px solid grey; margin:7px; padding: 7px"> |
|
<p>For now, I'm ignoring Tokio and async for next examples.</p> |
|
|
|
</div> |
|
<p>And because there is no "Post Office" kind of solver in Rust, I short-circuited |
|
the actors by giving the <code>Sender</code> channel of an actor as parameter to a second, |
|
so it knows where to send its messages. Something like:</p> |
|
<pre data-lang="rust" style="background-color:#2b303b;color:#c0c5ce;" class="language-rust "><code class="language-rust" data-lang="rust"><span style="color:#b48ead;">let</span><span> channel3 = actor3::run(...); |
|
</span><span style="color:#b48ead;">let</span><span> channel2 = actor2::run(channel3); |
|
</span><span>actor1::run(channel2); |
|
</span></code></pre> |
|
<p>In this short sample, whatever "actor1" produces, it sends directly to "actor2" |
|
though the channel the latter created; "actor2", on its part, produces |
|
something that is received by "actor3". And, with more actors, things just keep |
|
chaining.</p> |
|
<div style="border:1px solid grey; margin:7px; padding: 7px"> |
|
<p>I am intentionally ignoring the internals of each actor and their <code>run()</code> |
|
function, but they are some variations of:</p> |
|
<pre data-lang="rust" style="background-color:#2b303b;color:#c0c5ce;" class="language-rust "><code class="language-rust" data-lang="rust"><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">run</span><span>(..) -> (task::JoinHandle<()>, mpsc::Sender<TheKindOfMessageTheActorAccepts>) { |
|
</span><span> </span><span style="color:#b48ead;">let </span><span>(tx, </span><span style="color:#b48ead;">mut</span><span> rx) = mpsc::channel::<TheKindOfMessageTheActorAccepts>(</span><span style="color:#d08770;">SOME_SIZE</span><span>); |
|
</span><span> </span><span style="color:#b48ead;">let</span><span> task = tokio::spawn(async </span><span style="color:#b48ead;">move </span><span>{ |
|
</span><span> </span><span style="color:#b48ead;">while let </span><span>Some(incoming) = rx.</span><span style="color:#96b5b4;">recv</span><span>().await { |
|
</span><span> </span><span style="color:#b48ead;">let</span><span> conversion = </span><span style="color:#96b5b4;">actor_process</span><span>(incoming); |
|
</span><span> </span><span style="color:#65737e;">// maybe send the conversion to the next actor? |
|
</span><span> } |
|
</span><span> }); |
|
</span><span> (task, tx) |
|
</span><span>} |
|
</span></code></pre> |
|
|
|
</div> |
|
<p>But... 'cause the actors have (very similar) interfaces, that looks like a |
|
trait!</p> |
|
<p>So, what should be the Actor trait?</p> |
|
<p>First thing, its <code>run()</code> or similar function should expose its PID and its |
|
receiving channel. Something like:</p> |
|
<pre data-lang="rust" style="background-color:#2b303b;color:#c0c5ce;" class="language-rust "><code class="language-rust" data-lang="rust"><span style="color:#b48ead;">pub trait </span><span>Actor { |
|
</span><span> </span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">run</span><span>() -> (task::JoinHandle<()>, Sender<TheKindOfMessageTheActorAccepts>); |
|
</span><span>} |
|
</span></code></pre> |
|
<p>Why <code>TheKindOfMessageTheActorAccepts</code>? That's because each actor may have a |
|
different input message. If we take our short sample above, "actor2" may be |
|
receiving <code>usize</code>s and sending them as <code>String</code>s to "actor3".</p> |
|
<p>Because that type may change from actor to actor, it should be an associated |
|
type:</p> |
|
<pre data-lang="rust" style="background-color:#2b303b;color:#c0c5ce;" class="language-rust "><code class="language-rust" data-lang="rust"><span style="color:#b48ead;">pub trait </span><span>Actor { |
|
</span><span> </span><span style="color:#b48ead;">type </span><span>Input; |
|
</span><span> |
|
</span><span> </span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">run</span><span>() -> (task::JoinHandle<()>, Sender<</span><span style="color:#b48ead;">Self::</span><span>Input>); |
|
</span><span>} |
|
</span></code></pre> |
|
<p>So the basic idea is that, once the trait is implemented in a struct, we could |
|
managed it like:</p> |
|
<pre data-lang="rust" style="background-color:#2b303b;color:#c0c5ce;" class="language-rust "><code class="language-rust" data-lang="rust"><span style="color:#b48ead;">let</span><span> actor3 = Actor3::new(..); |
|
</span><span style="color:#b48ead;">let </span><span>(actor3_pid, actor3_channel) = actor3::run(); |
|
</span></code></pre> |
|
<p>Wait, what about the chaining? We could do something simple like:</p> |
|
<pre data-lang="rust" style="background-color:#2b303b;color:#c0c5ce;" class="language-rust "><code class="language-rust" data-lang="rust"><span style="color:#b48ead;">let</span><span> actor3 = Actor3::new(..); |
|
</span><span style="color:#b48ead;">let </span><span>(actor3_pid, actor3_channel) = actor3::run(); |
|
</span><span style="color:#b48ead;">let</span><span> actor2 = Actor2::new(actor3_channel); |
|
</span><span style="color:#b48ead;">let </span><span>(actor2_pid, actor2_channel) = actor2::run(); |
|
</span></code></pre> |
|
<p>... which is kinda verbose, but does work.</p> |
|
<p>I have some ideas to make this part more fluent, but I need to do some more |
|
exploration about the topic (specially since I think we can leverage the type |
|
system to not allow connecting actors whose input type is not the same as the |
|
output type of the previous actor). Once I get those hammered down, I'll get a |
|
follow up post.</p> |
|
<!-- |
|
vim:spell: |
|
--> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
</div> |
|
|
|
</body> |
|
|
|
</html>
|
|
|