Introduction to the Actor-based concurrency model
In software development, performance benefits from multi-core processors can be leveraged to the maximum through concurrent programming…
In software development, performance benefits from multi-core processors can be leveraged to the maximum through concurrent programming. Concurrent programming is designed to create independent processes working parallelly in a specific composition without affecting the desired outcome. Various concurrency models are being used such as threads and locks, Futures, coroutines, Communicating Sequential Processes, Actor model etc. In this blog, we will focus on the actor model and try to understand its working, pros and cons.
What is an Actor?
An actor is a lightweight high-level abstraction for asynchronous message passing and computation. Actors are isolated from each other and do not share memory. An actor is considered the universal primitive in the actor model (i.e.) all possible operations with the data like processing, storing and communication happen only through actors.
An actor comprises of
Mailbox: A message queue-like data structure for storage.
Mailing address: For identifying and communicating with other actors.
State: Certain variables contained within the actor constitute its state of it.
Behavior: Decided upon by incoming message which could be a simple function to be applied to the input.
What can an Actor do?
Communicate with other actors by exchanging messages asynchronously.
Change its own internal behavior/state.
Process the received messages in the mailbox in a FIFO fashion.
Create multiple actors, supervise other actors etc.
How does the Actor model work?
Computation of tasks is a result of communication happening in the system. The tasks are received by an actor in the form of messages in linear order and processed in a FIFO fashion. Based on the incoming messages, the actor’s behavior is configured, and the necessary action is performed on the received message to produce the desired response. The response is then passed on to the actor of interest. An actor can either process the task by itself or create new actors and new tasks for the completion of the same. The creation of new actors for the execution of tasks is where concurrency comes into the picture. Depending on the requirement and hardware specs, any number of actors can be created and since the actors do not share their internal state, tasks can be processed by actors concurrently. Completed tasks and unused actors will be safely removed from the actor model without impacting the execution of other tasks.
Message delivery in the Actor model
The actor model offers at most one message delivery guarantee when it comes to message passing. This means that any message being sent from one actor to another might either be delivered once or not delivered at all. At most one delivery is chosen for its high-performance benefits because of reduced overhead compared to at least once or exactly once which might require retries to achieve them. Usage of the at most delivery pattern is highly desirable in various use cases where performance takes the upper hand and occasional loss of messages doesn’t disrupt the functioning of the system.
Error handling in the Actor model
Most actor model implementations use the notion `Let it crash` wherein failure of actors is an allowed phenomenon. However, the failure of an actor doesn’t go unnoticed. The actor model follows a hierarchical structure where every actor has a supervising actor. The supervising actor is notified of the failure of the subordinate actor and is primarily responsible for handling the same. Whenever the supervisor actor receives a notification of failure, it executes the error handling mechanism specified in the actor model implementation on the failed subordinate actors. Common error handling mechanisms include ignoring the error, restarting the actor, escalating the error to the supervisor of the supervising actor, killing the actor in case of unrecoverable failures etc.
How Actor model is different from other concurrency models?
In most concurrency models, one thread per concurrency primitive approach is used. In the actor model of concurrency, Actors aren’t restricted to single thread usage for the completion of a task. Subsequent invocations of instructions for a task from an actor can happen on different threads. Also, a single thread can be used for task execution from multiple actors. In this way, actors don’t hold ownership of the running thread. This makes actors lightweight entities compared to threads and opens possibilities for the creation of a huge number of actors for concurrency.
Popular Actor model implementations
The actor model is not just a conceptual model. There are various popular frameworks/libraries existing in the majority of the languages achieving the actor model concepts mentioned in this blog. Let’s look at two of the most popular implementations of the actor model.
Erlang
Erlang is a language designed by Ericsson to program large highly reliable fault-tolerant telecommunications systems. The Erlang virtual machine is designed to manage many lightweight processes on top of the OS processes. These lightweight processes have all the characteristics of an actor such as immutable state, message passing, and fault tolerance. The Erlang Virtual Machine (BEAM) itself manages the concurrency, monitoring and garbage collection for these process-based actors providing built-in support for actor-based concurrency.
AKKA
Akka is an actor-based concurrency framework for JVM-based languages. JVM doesn’t have inherent support for actors. Scala Actors was the pioneer in bringing the erlang model of actors into JVM. Scala actor model was built by efficiently multiplexing actors to OS threads. AKKA is based on Scala actors and was more actively developed compared to the scala actors to provide better performance benefits. AKKA is actively being used to build highly scalable production-ready applications. Akka actors can communicate with actors with actor references within as well as across systems which enables support for distributed microservices architecture. AKKA cluster infrastructure provides features like discoverability, load balancing, sharding, and node monitoring that in many ways eliminates the complexity and cost involved in setting up the microservices architecture. Efficient load and performance testing tools like Gatling have also been built from AKKA.
Understanding a simple Actor system with AKKA:
Let’s build a simple system for detecting and tracking palindrome words in a given paragraph. This requirement can be further broken down into simpler tasks as follows,
Tokenize a paragraph into words.
Identify whether the words are palindrome or not.
Keep track of the palindrome occurrences.
There are going to be multiple words in a paragraph. Determining whether a word is a palindrome or not is an independent task determined by the input word alone. This makes this task suitable for concurrency. Let’s try to use the actor model for achieving this concurrency in our system.
The AKKA actor system for our problem statement comprises a StringProcessor actor that gets a Request message as a paragraph. The StringProcessor tokenises the paragraph into words and spawns PalindromePrintor actors for each word in the paragraph and sends the single word as an Input message to the spawned actors. The PalindromePrinter actors execute the required logic and find if the string is palindrome or not and print the same. Additionally, whenever a palindrome string is encountered, the PalindromePrinter actors send a PalindromeEvent message to the PalindromeCountTracker. The PalindromeCountTracker actor simply increments the counter and prints it.
Implementing the Actor model for the above example with AKKA
For using AKKA in our project we need to include the maven dependency from Maven Repository: com.typesafe.akka » akka-actor (mvnrepository.com) Once added we will be able to use the AKKA implementation to use actors for building our distributed concurrent system.
The first step is to create the system for the actors to exist which will allocate the threads for processing. While creating the actor system, a top-level actor called the guardian actor is expected as a parameter. This Guardian actor should be able to handle the messages being sent into the actor system.
ActorSystem system = ActorSystem.create(StringProcessor.create(),"palindrome-system");In our system, the StringProcessor is the guardian actor. The StringProcessor will accept the request containing a paragraph and spawn the words in the paragraph as Input messages to various child PalindromePrinter actors for further processing.
We need to define three actor classes (
StringProcessor,PalindomePrinter,PalindromeCountTracker) for the above system. To keep it simple we will look at the PalindromePrinter class definition alone. The other two classes can be defined in a similar fashion. For creating an actor-based class, we need to extend the AbstractBehaviour class of AKKA with a template parameter as the type of message the actor can accept.
public class PalindromePrinter extends AbstractBehavior<PalindromePrinter.Input> {
public static final class Input {
String message;
ActorRef<PalindromeCountTracker.PalindromeEvent> replyTo;
public Input(String message,ActorRef<PalindromeCountTracker.PalindromeEvent> replyTo) {
this.message=message;
this.replyTo=replyTo;
}
}
public static Behavior<Input> create() {
return Behaviors.setup(PalindromePrinter::new);
}
@Override
public Receive<Input> createReceive() {
return newReceiveBuilder().onMessage(Input.class,this::checkAndPrintIfPalindrome).build();
}The PalindromePrinter class can be defined as above. Here Input is a static nested class that defines the type of message the PalindromePrinter actor accepts. Input class is having data members message of type string and replyTo of type ActorRef to store the reference of any actor that it needs to send a message. Once the actor class and input type are defined we need to override createReceive() method of the AbstractBehaviour to specify how the actor will process the message. Here we are specifying the function checkAndPrintIfPalindrome to be used for processing any Input message received.
The function
checkAndPrintIfPalindromeis applied to any Input object received byPalindromePrinter. ThePalindromePrinteruses theIsPaindromefunction on the input message to decide and print if the string is palindrome or not. ThePalindromPrinteralso sends aPalindromeEventto thePalindromeCountTrackerActor whenever theisPalindromefunction returns true. ThePalindromeEventcan be sent by invoking the tell(message) method on the recipient actor ref as shown below.
private Behavior<Input> checkAndPrintIfPalindrome(Input input) {
if (isPalindrome(input.message)) {
input.replyTo.tell(new PalindromeCountTrack-er.PalindromeEvent(true));
getContext().getLog().info("\"{}\" is a palindrome", input.message);
} else {
getContext().getLog().info("\"{}\" is not a palindrome", input.message);
}
return Behaviors.same();
}On receiving this PalindromeEvent message the PalindromeCountTracker increments its internal counter and prints the same.
After the other two actors are defined in a similar way, we can send the first message to the guardian actor of our system using the following code.
system.tell(new StringProcessor.Request("Wow, It is a beautiful day."));Upon sending the paragraph as message to the StringProcessor actor, each word is processed by the actors in the system asynchronously and the result is printed as shown below.
[2022-07-11 10:15:28,647] [INFO] [com.custom.StringProcessor] [palindrome-system-akka.actor.default-dispatcher-3] [akka://palindrome-system/user] - Processing paragraph....Wow, It is a beautiful day.
[2022-07-11 10:15:28,666] [INFO] [com.custom.PalindromePrinter] [palindrome-system-akka.actor.default-dispatcher-7] [akka://palindrome-system/user/It0.142426711304806] - "It" is not a palindrome
[2022-07-11 10:15:28,666] [INFO] [com.custom.PalindromePrinter] [palindrome-system-akka.actor.default-dispatcher-8] [akka://palindrome-system/user/is0.6628995083209714] - "is" is not a palindrome
[2022-07-11 10:15:28,666] [INFO] [com.custom.PalindromePrinter] [palindrome-system-akka.actor.default-dispatcher-6] [akka://palindrome-system/user/Wow0.34866621520695096] - "Wow" is a palindrome
[2022-07-11 10:15:28,667] [INFO] [com.custom.PalindromePrinter] [palindrome-system-akka.actor.default-dispatcher-8] [akka://palindrome-system/user/a0.29580985088847256] - "a" is a palindrome
[2022-07-11 10:15:28,667] [INFO] [com.custom.PalindromeCountTracker] [palindrome-system-akka.actor.default-dispatcher-7] [akka://palindrome-system/user/palindromeTracker] - Until now 1 palindrome events have occurred
[2022-07-11 10:15:28,667] [INFO] [com.custom.PalindromeCountTracker] [palindrome-system-akka.actor.default-dispatcher-7] [akka://palindrome-system/user/palindromeTracker] - Until now 2 palindrome events have occurred
[2022-07-11 10:15:28,667] [INFO] [com.custom.PalindromePrinter] [palindrome-system-akka.actor.default-dispatcher-8] [akka://palindrome-system/user/beautiful0.22490422686520395] - "beautiful" is not a palindrome
[2022-07-11 10:15:28,668] [INFO] [com.custom.PalindromePrinter] [palindrome-system-akka.actor.default-dispatcher-8] [akka://palindrome-system/user/day0.1723735260989474] - "day" is not a palindromeNote: The above code snippets are just for understanding core concepts. Fully functional implementation can be viewed in aymar99/Akka-simple-example: This repository consists of a small example to depict the actor concurrency model. (github.com)
Benefits of using the Actor model
Low-level concurrency programming involves handling shared mutable states. Race conditions, deadlocks and many other concurrency problems need to be addressed for the same. Locking, blocking and various synchronisation mechanisms need to be used to address the mentioned problems in low-level concurrency programming. The actor model provides a high-level abstraction with all these problems addressed in the most efficient way providing thread-safe concurrency with minimal programming effort.
The actor is a lightweight concurrency primitive with an isolated state. Hence, hundreds and thousands of actors can be spawned for creating highly concurrent and performant software applications.
Actors can pass messages to actors outside of the system over the network same as it does within the system. This paves way for scaling concurrency both within and across the systems with ease.
Fault tolerance is provided in the actor model by organising the actors in a tree structure where every actor has another supervisor actor on top of its hierarchy. The supervising actors have provision to specify the error handling mechanisms to be executed in case of unexpected failures. This hierarchical structure creates self-healing systems with the capability to easily detect and react to failures.
Caveats of using the Actor model
The message passing is asynchronous hence the order of messages cannot be guaranteed. This makes it not suitable for use cases where the sequencing of tasks is important.
Debugging of production failures might be complex without necessary logging since actors are abstractions and are not tied to any specific thread.



