A Tour To Akka Actor Underneath

Akka Actor is a concurrency framework built in Scala and Java, aiming to provide a simple and straightforward abstraction to model your components, while decoupling multi-threading away from the business logic.

With Akka Actor, you can model your components naturally and straightforwardly, just like designing a role in the real world who can receive a set of specific messages, react on each message correspondingly, and optionally send one or more messages to other coworkers. This allows you to focus on how the roles behave while designing your system, and Akka will take care of managing the concurrent execution.

Akka Actor also offers efficient concurrency for your system. Threads and work scheduling are managed by Akka itself, where Akka use threads only on the actors that have actual works to do for particular actors, hence not holding threads waiting and wasting resources. Communication between actors are usually via sending messages to the mailbox of the target actor, and the actor will process the messages from its mailbox sequentially in a FIFO order. Therefore we can model our system in an asynchronous style quite naturally. (But Akka does provide you approaches for synchronous communication if you need.)

Now let’s look at the source code to understand how this works.

Basic functionality is defined in a trait named Actor. The core method is receive, that’s where we define how the actor reacts to each type of the message it supports. It’s a partial function so you can implement each message type per case and add new cases when new message types come. The trait also provides lifecycle hook methods to allow you customize lifecycle control on the actor.

Typically we should access an Actor only via its ActorRef, which refers to the Actor implementation and provides some common functionality like the path to the actor, method to send/forward message to this Actor etc. The ActorRef is further encapsulated in an ActorCell which contains the context around the actor, including:

  • the ActorSystem it’s in
  • the ActorRef referring to itself
  • the Props object used when creating the Actor
  • the MessageDispatcher which handles how to schedule actors to work on the thread pool
  • the ActorRef to its parent actor
  • a behaviorStack to keep a collection of behaviours defined on the actor in a LIFO order
  • a sysmsgStash to temporarily stash system messages

the ActorCell also has access to the MailBox associated to its actor. That is defined in the dungeon.Dispatch trait.

After having a look at how the basic skeleton of Akka actor is, let’s take a tour to see how an Actor is created and started processing, also how communication between actors works.

Actors run in ActorSystem. The first thing is to start the ActorSystem before we can spawn actors inside it. The implementation is in ActorSystemImpl class.

When the ActorSystem is bootstrapped, initially it contains only one “guardian” actor, and all the other actors in the same actor system will be the guardian’s direct or indirect children. This allows us to organize our actors in a hierarchical structure where a parent actor supervises and manages its own child actors, and avoid the guardian being overwhelmed by managing large quantity of actors.

Calling ActorSystem.actorOf(props: Props) method will create a top-level actor. It delegate the real creation to the ActorCell’s ActorRefProvider, then call ActorRef.start() to bootstrap the actor and return the actor reference.

Then what does the ActorRef.start() do? It delegates the call to dungeon.Dispatch, one of the traits that ActorCell extends.

It says “start off the actor by scheduling its mailbox”, not “scheduling the actor cell”?

It turns out that, Mailbox, rather than Actor, is the task submitted to thread pool or fork-join pool. Mailbox.run() shows that when executed, the mailbox first check whether itself is closed, if not then process all the system messages, followed by processing messages in the mailbox queue. When it finished processing messages, we need to reschedule the mailbox for next execution. It will be re-submitted to ExecutionService if there’s still unprocessed system or mailbox messages in the mailbox.

The process of handling system messages and mailbox messages are quite similar, except that all system messages in queue are processed at once but mailbox ones are processed one at a time. A deadline is set to prevent a mailbox from using the thread for too long and causing others waiting for too long.

That’s what happens when we create and start a new actor. Next we’ll see how a message is sent to an actor.

You can send any immutable object as a message to an ActorRef, by calling ! method like below:

actorA ! DoA("details...")

the ! method then call sendMessage on the corresponding ActorCell, which is implemented in dungeon.Dispatch trait.

So every time we send a message to an actor’s mailbox, the mailbox will be scheduled by ExecutorService to process when thread resource is available.

While reading the source code, the basic understandings I got are as below:

  • Akka Actor framework uses ExecutorServices to concurrently schedule and execute messages in the mailboxes of actor instances.
  • It’s the mailbox that implements Runnable and got executed by the ExecutorServices. When executing, each mailbox apply the corresponding Actor.receive function to messages in the mailbox in a FIFO approach.
  • You can think of an actor as a Runnable task that has a queue to store messages sent to it, and defines behaviours on how to process each type of message it supports. The tasks can communicate only via sending messages.
  • The dispatching of messages happen concurrently without ordering guarantee, but messages received by a specific actor will be processed in a sequential order.