When I write about Akka.NET and the actor model, this is not what I mean:

Not the right kind of Actor model

I’m probably going to abuse this stupid joke in every 100-level blog post and video where I introduce the concept of Actors, so please bear with me.

In all seriousness, an actor is a broad concept. Here’s my layman’s definition of the actor model:

An “actor” is really just an analog for human participants in a system. Actors communicate with each other just like how humans do, by exchanging messages. Actors, like humans, can do work between messages.

Think of something like a call center, where hundreds of customers might call a 1-800 number and have concurrent conversations with one of many possible customer service agents.

An actor is just an analog for human participants in a system.

This type of interaction can be modeled using actors.

An actor is just an analog for human participants in a system.

In the Actor model, everything is an actor. Just like how everything is an “object” in Object Oriented Programming (OOP.) When working in C#, you have to model your problem domain using classes and objects. When working with Akka.NET, you model your problem domain with actors and messages.

Here’s an example of what the most basic UntypedActor looks like in Akka.NET:

using System;
using Akka.Actor;

namespace ActorsSendingMessages
{
    /// <summary>
    /// See http://akkadotnet.github.io/wiki/Actors for more details about
    /// Akka.NET actors
    /// </summary>
    public class BasicActor : UntypedActor
    {
        protected override void PreStart()
        {

        }

        protected override void PreRestart(Exception reason, object message)
        {

        }

        protected override void OnReceive(object message)
        {
            //handle messages here
        }

        protected override void PostStop()
        {

        }

        protected override void PostRestart(Exception reason)
        {

        }
    }
}

There are many types of Actors in Akka.NET, but all of them derive from the UntypedActor.

Why Use Akka.NET?

What are the compelling reasons for using Akka.NET? What sorts of problems can it solve for you and your company?

Click here to subscribe to our mailing list and download our "Why Use Akka.NET?" white paper.

What’s A Message?

You might have noticed the following method on my BasicActor definition above:

protected override void OnReceive(object message)
{
    //handle messages here
}

The OnReceive method is where any actor receives its messages. In Akka.NET a “message” is just a C# object. A message can be an instance of a string, an int or a user-defined class like OfficeStaff.RequestMoreCoffee.

Actors are typically programmed only to handle a few specific types of messages, but nothing bad happens if an actor receives a message it can’t handle. Typically it just logs the message as “unhandled” and moves on.

Messages Are Immutable

Time for a fun developer buzzword that will make you sound really smart: immutability!

So what’s an “immutable” object?

An immutable object is an object who’s state (i.e. the contents of its memory) cannot be modified once it’s been created.

If you’re a .NET developer, you’ve used the string class. Did you know that in .NET string is an immutable object?

Let me give you an example:

var myStr = "Hi!".ToLowerInvariant().Replace("!", "?");

In this example, the following occurs in this order:

  1. .NET allocates a const string with the content Hi!” and then
  2. ToLowerInvariant() is called, which copies the original “Hi!” string and modifies the copy to become all lowercase and returns the modified copy (which now reads as “hi!”) Then
  3. .Replace(string,string) is called, which copies the “hi!” string returned by ToLowerInvariant and modifies the copy to substitute ! with ? and returns the modified copy, with the final result reading as “hi?”

Since the original string is immutable, all of these operations had to make a copy of the string before they could make their changes.

Immutable messages are inherently thread-safe. No thread can modify the content of an immutable message, so a second thread receiving the original message doesn’t have to worry about a previous thread altering the state in an unpredictable way.

Hence, in Akka.NET - all messages are immutable and thus thread-safe. That’s one of the reasons why we can have thousands of Akka.NET actors process messages concurrently without synchronization mechanisms and other weird stuff - because immutable messages eliminate that as a requirement.

Actor Behavior 101

So we know roughly what components go into an actors and messages… How does this stuff actually work?

Actors Communicate Via Message Passing

In OOP, your objects communicate with each-other via function calls. The same is true for procedural programming. Class A calls a function on Class B and waits for that function to return before Class A can move onto the rest of its work.

In the Akka.NET and the Actor model, actors communicate with each-other by sending messages.

Actors communicate by passing messages to each other.

So what’s so radical about this idea?

Well for starters, message passing is asynchronous - the actor who sent the message can continue to do other work while the receiving actor processes the sender’s message.

So in effect, every interaction one actor has with any other actor is going to be asynchronous by default.

That’s a dramatic change, but here’s another big one…

Since all “function calls” have been replaced by messages, i.e. distinct instances of objects, actors can store a history of their function calls and even defer processing some function calls until later in the future!

Imagine how easy it would be to build something like the Undo button in Microsoft Word with an actor - by default you have a message that represents every change someone made to a document. To undo one of those changes, you just have to pop the message off of the UndoActor’s stash of messages and push that change back to another actor who manages the current state of the Word document. This is a pretty powerful concept in practice.

Actors Send Messages to Actor References, Not Directly to Actors

Ready for another buzzword? Location transparency. Actors have it.

What location transparency means is that whenever you send a message to an actor, you don’t need to know where they are within an actor system, which might span hundreds of computers. You just have to know that actors’ address.

Actors communicate via addresses, just like people do with cell phone numbers.

Think of it like calling someone’s cell phone number - you don’t need to know that your friend Bob is in Seattle, Washington, USA in order to place a call to them. You just need to dial Bob’s cell phone number and your cellular network provider will take care of the rest.

Actors work just the same way - in Akka.NET every actor has an address that contains the following parts:

Akka.NET actor address and path

  • Protocol - just like how you can have HTTP and HTTPS on the web, Akka.NET supports multiple transport protocols for inter-process communication. The default protocol for single-process actor systems is just akka://. If you’re using remoting or clustering, you’ll typically use a socket transport like akka.tcp:// or akka.udp:// to communicate between nodes.
  • ActorSystem - every ActorSystem instance in Akka.NET has to be given a name upon startup, and that name can be shared by multiple processes or machines that are all participating in a distributed ActorSystem.
  • Address - if you’re not using remoting, then the address portion of an ActorPath can be omitted. But this is used to convey specific IP address / domain name and port information used for remote communication between actor systems.
  • Path - this is the path to a specific actor at an address. It’s structure just like a URL path for a website, with all user-defined actors stemming off of the /user/ root actor.

So when you want to send a message to an Actor, you send a message to an address:

//local actor
var actorRef = MyActorSystem.Selection("/user/myActor");
actorRef.Tell("HI!");

//remote actor
var remoteActorRef = MyActorSystem.Selection("akka.tcp://MyActorSystem@localhost:1001/user/myActor");
remoteActorRef.Tell("HI!");

Sending a message to a remote actor vs. a local actor looks exactly the same to you. That’s what location transparency means.

All Messages Sent to An ActorReference Are Placed In A “Mailbox” That Belongs to the Actor.

When you send a message to an actor, the message doesn’t go directly into an actor’s OnReceive method.

The message is placed into a “mailbox” that is sorted in FIFO order, just like an regular Queue<T> data structure. The mailbox has a really simple job - accept and hang onto messages until the actor is ready to process them.

When the actor is ready to process a message, the mailbox will push the message into the actor’s OnReceive method and run the actor’s message-processing methods.

Actors Only Process One Message at A Time

Take a number and stand in line, pal!

Akka.NET actors process messages serially

Ok, well it’s not really like that.

One of the guarantees actors in Akka.NET make is that an actor’s context and internal state are always thread-safe when processing a messages.

The reasons this is true are:

  • Because messages are immutable, so the content of each message is inherently thread-safe and
  • Because messages are processed serially, so changes to an actor’s internal state and context don’t have to be synchronized across multiple threads - therefore you can treat them like how you would in a single-threaded environment.

So an actor can’t begin processing a second message until its OnReceive method exits - at which point the mailbox will push the next available message into the OnReceive method.

Actors Can Have Internal State

Just like any other C# class, actors can have their own properties and fields.

When an actor restarts, the actor instance - like an instance of my BasicActor class in this case - is destroyed and recreated.

A new instance of BasicActor is created and any constructor arguments I passed to it via Props (a class we’ll talk more about in the future, but for now think of Props as a recipe for creating actors) will be passed to the new instance again.

The reason I bring this up is because in the next section we’ll talk about the actor’s lifecycle, and it’s worth remembering that Akka.NET can reboot any of your actor classes back into their initial state whenever there’s an unexpected problem.

Actors Have A Well-defined Life Cycle

Before actors can begin processing messages from their mailbox, they have to be instantiated by the actor system and run through their life cycle.

Akka.NET actor life cycle steps.

Actors are created and started, and then they spend most of their lives receiving messages. In the event that you no longer need an actor, you can terminate or “stop” an actor.

In the event that an actor accidentally crashes (i.e. throws an unhandled Exception,) the actor’s supervisor will automatically restart the actor’s lifecycle from scratch - without losing any of the remaining messages still in the actor’s mailbox.

I actually illustrated all of the Akka.NET methods that implement this lifecycle in BasicActor sample earlier - let’s go through them:

  • Actor's constructor - I didn’t illustrate this in the BasicActor sample because I used a defaul constructor there, but you have the ability to pass arguments into the constructor of your actor just like any other class in C#.
  • PreStart - this logic gets run before the actor can begin receiving messages and is a good place to put initialization logic. Gets called during restarts too.
  • PreRestart - if your actor accidentally fails (i.e. throws an unhandled Exception) the actor’s parent will restart the actor.
  • PostStop - gets called once the actor has stopped and is no longer receiving messages. This is a good place to include clean-up logic. PostStop does not get called during actor restarts - only when you’re intentionally shutting down an actor.
  • PostRestart - gets called during restarts after PreRestart but before PreStart. This is a good place to do any additional reporting or diagnosis on the error that caused the actor to crash, beyond what Akka.NET already does for you.

Here’s what the Akka.NET methods fit into the lifecycle I showed you earlier:

Akka.NET actor life cycle steps with explicit methods.

Every Actor Has A Parent, And Some Have Children

Just like people, actors have parents - and some have grandparents, siblings, and children.

Akka.NET actor hierarchy - which looks a lot like a family tree

Whaa? What does this mean?

It means that every actor has to be created by some other actor. So when we write code like this:

var actorRef = MyActorSystem.ActorOf(Props.Create<BasicActor>(), "myActor");
actorRef.Tell("HI!");

We’re really creating a new child actor on the /user/ root actor - so the path of this newly created actor is actually /user/myActor/.

Similarly, when we’re running inside this BasicActor we can create other child actors:

protected override void OnReceive(object message)
{
    var childActor = Context.ActorOf(Props.Create<BasicChildActor>(), "child1");
    childActor.Tell("Hi!");
}

In this case, the childActor path will be /user/myActor/child1/.

Now… as to why this is important…

Parents Supervise Their Children

In the section about actor’s lifecycles, I mentioned the concept of “actors being restarted by their supervisors.” Here’s what I mean - every parent actor receives special “Help, I’m crashing!” messages from their children.

Parents come with a default SuperviserStrategy object (or you can provide a custom one) that makes decisions on how to handle failures with their child actors. Parents can make one of the following three decisions:

  • Restart the failed actors, which the parents will do by default unless the child has failed repeatedly over a small period of time, like 60 seconds;
  • Stop the failed actors, which permanently terminates them; or
  • Escalate the supervision to the grand-parent actor.

When a Restart or Stop directive is issued, all children of the affected actor will themselves be restarted or killed too. In effect, you can reboot the entire part of the actor family tree that’s been failing all at once.

This is a really powerful concept for reliability and self-healing systems, which we’ll cover in more detail in the future.

Wait… How Can Actors Be Massively Concurrent If All Of This Is True?

Actors can only process one message at a time? Isn’t that super duper slow?

This is a great question. Here’s the break down of how you turn these actors into a concurrent processing mega-machine.

Actors Are Cheap

According to our benchmarks on the Akka.NET website, you can fit 2.5 million actors into a single Gigabyte of RAM. We’ve actually made some significant memory optimizations since then, so we can probably fit a lot more into memory now.

But the point is - actors are inexpensive and you can use a lot of them.

Actors Scale Out

If actors are inexpensive, then this means that the memory overhead of using one actor versus a thousand actors is trivial. And….

One actor can only process one message at a time… But a thousand actors can process one thousand messages at a time. That’s how you unlock the concurrent processing potential of an actor system - by scaling out with lots of actors.

In other words, you get the most value out of actors when you use many of them. If you wrote your entire application to only use a single actor it would be hideously slow.

So design your actor system to scale out in order to take full advantage of the actor model’s concurrent processing capability.

Actors Are Lazy

So what happens if an actor doesn’t have any messages to process?

Akka.NET actors are lazy - they don't take up any resources if there's no work to be done.

Yes, that’s a mailman sleeping inside a mailbox - just in case you were wondering.

If there aren’t any messages to process, the actor doesn’t do any work. All of the other actors who actually have messages to process are the ones who’ll actually use threads and your server’s processing power.

Actors who don’t have any messages don’t take up any threads or any other compute resources. They’re reactive - they wait to get woken up by an arriving message.

This is one of the reasons actors are so cheap and thus, scale out well.

If you want to learn more about Akka.NET and other topics related to distributed computing in .NET, make sure you sign up for our mailing list below!

If you liked this post, you can share it with your followers or follow us on Twitter!
Written by Aaron Stannard on January 25, 2015

 

 

Observe and Monitor Your Akka.NET Applications with Phobos

Did you know that Phobos can automatically instrument your Akka.NET applications with OpenTelemetry?

Click here to learn more.