There are a couple of actor libraries available for Rust, the most well known one being Actix. I found the library not that easy to use however, especially when using async/await. After getting to know Tokio a little better (the runtime which Actix also uses), I realised that the basic building blocks for creating an actor like framework are available in Tokio itself. In simplest terms, you can use Tokio’s channels to pass messages around to structs that then act like actors. You just need to define handlers on the struct that define the behaviour you want per message type.
So to structure my existing code a little better, I decided to create a tiny framework that makes it easier for me to implement an actor model on top of Tokio. From there the Tiny Tokio Actor crate was born.
To use it, simply add the following dependency to your project:
tiny-tokio-actor = "0.2"
Next, import the crate and define a message you want to have the ActorSystem use in its Event Bus. Each ActorSystem has an event bus that all actors that run on it can send and receive events on. You can define a struct as a
SystemEvent as follows:
(if you want to copy/paste the code, the complete gist is on github)
The struct must derive
Debug is entirely optional. With this done, we can go and create the actor system on which we will run our actors:
To create the system, we first need to create it’s message bus. We create a message bus that will send/receive
TestEvent messages and will have a buffer of 1000 messages.
With our actor system created, let’s define an actor struct that we will run on this system:
post_stop() methods is entirely optional, the defaults do nothing. These methods are good if you want to initialise your actor in some way, and also control its “shutdown”.
Notice also that we need to decorate the
impl block with
#[async_trait] as we use async functions inside the trait. See the async-trait crate for more info about this. Note that this crate re-exports
async-trait so there is no need to add it as a separate dependency.
After defining the Actor, next we define the messages we like to send to it:
The message struct must derive
Debug is optional. We also define what response we want the Actor to send back when it receives this message. That is what the
Response type is for. Here we define it as returning a
Now that we have our message, we can go ahead and define the behaviour our actor will have when it receives the message:
The behaviour is very simple: when we receive
TestMessage we publish a message on the actor system’s event bus, increase the internal counter by 1, and send back a String containing “Ping!”.
If we want our actor to handle another message type, simply create that message type first, and then implement the
Handler for that other type. For example:
OtherMessage is. very similar to the pervious message, except that it holds a
usize instead of
String, and also send back a
usize instead of
String. We can see the behaviour first publish a message on the system’s event bus, and then set the actor’s counter to the value of the message, and then return the value of its counter.
Now that we have. our actor and it’s behaviour defined, we can run it on our actor system. First though, let’s set up a loop that listen’s our actor system’s event bus:
Now launch the actor on the system:
When we create the actor, the actor system will return an
ActorRef to it. This actor reference allows you to send messages to the actor. For example:
ask the actor a message, which means we also want a response. We can also
tell the actor a message, in which case the message sending becomes “fire-and-forget”, i.e. we do not wait for a response.
So there we have it, an actor system running one actor on top of Tokio. Here is the code all in one gist:
You can also check out the code in the project here.
Using this actor framework allow me to organise my Tokio code a little better, but all the usual caveats apply! Do not block the event loop, hand over compute intensive tasks to rayon. Also be mindful of deadlocks. Actors do not save you from this, especially if you have actors sending messages to each other in a loop without back-pressure. Since this crate uses Tokio’s unbounded channel for an actors mailbox, if one actor is a little slow while other actors that send it messages are fast, the slow actor’s mailbox may fill up and use all resources and might eventually cause a deadlock if your actor graph is not acyclic.
So this little crate can help with structuring your code, but it will not save you from the usual concurrency problems that come with unbalanced producers/consumers. To read up more about this, I highly suggest reading Actors with Tokio on rhyl.io, including its accompanying reddit post.