Demystifying concurrency using Actors, Let there be Implementation (Part 2)
If you haven’t read the Part 1 of this blog series, I strongly encourage you to go through it and only then continue with this blog. The Part 1 covers all the conceptual basics that you need to start working with akka. Follow the link below.
Demystifying concurrency using Actors, Let there be Abstraction (Part 1)
What is Akka?
Akka is a free and open-source toolkit and runtime simplifying the construction of concurrent and distributed applications on the JVM. Akka supports multiple programming models for concurrency, but it emphasizes actor-based concurrency, with inspiration drawn from Erlang. Language bindings exist for both Java and Scala. Akka is written in Scala and, hence is kinda more native and organic to use it in Scala. Actor Model is just closer to Functional Programming and it’s prettier to write in Scala rather than in Java. That’s why I chose Scala as our main language to walk through Akka. Plus, I wanted to learn little bit of Scala too. Why not learn new technology with new language? Akka is powered by company called Lightbend. It definitely has one of the best documentations I have ever seen. You can read it as a book. In case my blog makes you think about Actor Model, and in particular Akka, just visit Akka Docs and get started.
Actor Hierarchy
In actor model we have the thing called Actor Hierarchy. Just like filesystem, actors have hierarchical structure. If you create an actor from an actor, it becomes “child” actor, and will be one level lower in the hierarchy. Just like creating folders inside folders. As creator of Actor Model, Carl Hewitt once said:
One actor is not an actor, they come in systems.
In actor model, single actor has no value. We need a group/system of actors to communicate with each other and do some magic. A typical application structure in Akka would look something like this. We have an Actor System, it’s like Spring Context. A single container that holds all actors inside one application. Usually we have one Actor System per application. Then we have the root Actor, the boss, the controller, the Main Guardian. It’s like the Object class in Java. Every other actor by default is a child of this Guardian System Actor. All green actors, are created by user. As you can see from the illustration, actors have paths. Just like normal file-system. Child actors can be treated as nested files in the file-system.
Now let’s implement this actor structure using Akka.
First start creating Actors: A and B. As simple as you can see. Don’t bother with Scala’s object creation syntax. Just concentrate on Actors. We have 2 Actors. As you can remember from previous blog, actors have behaviour. It’s the essential part of actor existence. And the syntax to setup actors with the minimal message printing behaviour looks like this. Type of apply method is Behavior[String]. It means that both actors can accept messages only of type String(we will see other message types later, but for now String work for our purpose). After defining what type of messages our Actors can receive, we should define the actual Behaviour of an actor. Behaviors.receive takes 2 arguments. context is the “thing” that keeps this actor as part of the whole actor system context. message is the received message that is taken from the actors mailbox when it enters method body.
Receive method itself implements special logic(you can call it handler, but in akka we call it behaviour). For the simplicity let’s just print the actor path. You can see that we simply get it from context.self and the received message. For the final step, let’s look at the last line in both actor objects. In scala we don’t have return statements. The last line of the method by default is a return statement. It means, if you want to return something you just put it on the last line in the method body. Another thing we know from the previous blog is: Every actor should designate how it should handle the next messages it receives. Therefore every time we implement receive functionality we should return the “next” behaviour. In this case Behaviors.same means that each and every next message received by this actor should be handled with the same logic. (we’ll see later example for updated behaviour, but for now lets stick with it)
Great! We know how to setup ActorA and ActorB. Now it’s time to see how we create UserGuardian actor. UserGuardian actor has slightly different apply method. Behaviors.setup will run only once, something like constructor in OOP. setup method takes single context argument. First we print the UserGuardian path. Then comes the interesting part. Using context.spawn method we create UserGuardian’s child actors: ActorA and ActorB. We just pass their names to spawn as second argument. If you have any experience with Spring, spawning new actors is something like putting new beans in Spring Context. So that the whole system becomes aware of these actors.
But wait, what is that strange ! in the middle of the code? How can we use such symbols to define anything? Boom, here comes the magic of scala. In scala everything is pretty much a function, and you can call them whatever you want. In this case ! represents tell() function which is used to send/tell message to an actor. We send “Hello My Friend” messages to ActorA and ActorB. Remember everything in Akka actors, is asynchronous. Therefore the tell/send operation is also non-blocking. As for the last line in setup function, we are just telling UserGuardian actor, that the behaviour described above should only be valid for one setup invocation. Don’t pay much attention to Behaviors.empty at this point.
For the final part let’s just create the ActorSystem, and run our application. If you are familiar with java you will like this main method:)). We simply create main method and create actor-system. First argument of ActorSystem should be the user-guardian actor, which we choose for our application.
Here is the whole code for our example:
For code example visit the link: https://github.com/acho01/akka-blog/blob/main/src/main/scala/Example1.scala
Let’s run it and see what happens. Below you can see 2 different runs of this main method. The logs perfectly prove the asynchronous nature of actors. ActorA and ActorB are 2 different actors that do everything asynchronously, hence the result. Also pay attention to the paths. They look just like in the illustration above.
Actor model in general does not give any guarantees about message delivery order. Even in the case when 2 messages are sent to a single Actor. But Akka made slight modification in this part of the implementation. Suppose M1 and M2 messages are sent to Actor A in this order: M1 then M2
In contrast with original Actor Model, Akka provides guarantee that: If M1 message is delivered, it must be delivered before M2. It means that there is some kind of ordering guarantee when sending messages to a single actor in Akka. Akka also provides at-most-once delivery guarantee. at-most-once delivery delivery means that for each message handed to the mechanism, that message is delivered once or not at all; in more casual terms it means that messages may be lost.
Lightweight/Runtime threads in Akka
The idea of lightweight threads in akka is not new. This great approach has already been successfully implemented in Go. Go’s concurrency model is an implementation of another great concurrency mechanism called CSP (This is out of our scope but it’s definitely worth looking). Main thing in Go is that Go runtime schedules lightweight threads, which are not directly mapped to OS level heavyweight ones. This approach is developed as open source project loom for java as well. It remains kinda new technology and is included in early jdk 19 builds. This is also worth looking if you are a Java developer.
For now, let’s forget about other techs, and concentrate on Akka runtime and it’s lightweight concurrency using actors. In Akka, every actor can be treated as a traditional thread that we are used to daily. The main difference between Akka actors (“threads”) and JVM native threads is, that they are managed by Akka runtime, instead of having direct mapping with Kernel Threads. As for the implementation Akka simply uses JVM’s original ThreadPools on which it schedules actors. Actors are so lightweight that Akka can use multiple OS Threads to manage thousands of actors.
To demonstrate this great feature we will implement a simple word counter example in Akka. The full version of this example would be kinda big to write here, therefore we will just do it in very simple way. Let’s first consider the illustration below:
Note: I may use Controller and Worker addressing WordCountController and WordCounterWorker, so don’t get confused.
As you can see, WordCountController is just an actor that reads text file line-by-line and spawns(creates) new worker actor for every line. (Consider it something like creating new thread for every line). I know 3 threads is not a problem at all, but first let’s consider this small example. Every WordCounterWorker, simply receives a text message, calculates word count and sends result back to Controller as message. All the count result messages from workers are summed in controller. Finally we get total word count of the text file.
Let’s try to code this very example (If you don’t want to dive deep into implementation can directly move to the running part of the code)
We will introduce a couple of cool Scala features in this example. First let’s define our protocol the messages which we will need to setup the word counter application. In scala we can define things called case classes and use them to easily match with received messages. Don’t worry about matching for now. Just think about these messages.
trait in scala is like an interface in standard OOP. By Controller Messages, we mean, messages that can be sent to our Controller. There are 2 messages that our controller will accept. StartCounting is a message which will carry fileName with itself. Whenever our WordCountController receives StartCounting message, it will retrieve fileName from it and start reading lines from this file. Another message is ControllerWordCountReply, this is the message that CounterWorker will send to the controller with the count number in itself. That’s all for Controller Messages. As later we will see that these case classes identify actor types (what type of messages actor can receive), for controller with 2 concrete Messages we have a single general trait ControllerCommand which is “parent” to all controller messages.
As for the WorkerWordCountTask, this class will be used as a request message from controller to worker. It will carry a single line text and the reference of the controller actor. As you can guess actors do not work as standard request-response things. They are asynchronous, therefore we somehow need the reference (address) of the controller actor inside the worker to send back the counting result. That is replyTo in our case. As WorkerWordCountTask is the only message worker can receive let’s leave it without parent trait.
Great, thats it for the Protocol side. We basically defined the language on which our actors should communicate. Now let’s move forward and implement this protocol. We start with WordCounterWorker actor.
Now I’d like to introduce the beauty of Pattern Matching. Just like in many FP languages, we can directly provide cases for received message here in Scala. As we have derived WorkerWordCountTask as case class earlier, scala will enable us to use pattern matching. Scala will identify message in our actor and if it is of type WorkerWordCountTask(text, replyTo) it will run the corresponding handler. In this case first we print the thread info message. As you can see we are on JVM and we can directly call Thread.currentThread().getName from scala. When our worker actor will receive a message it will print “OS THREAD : Akka Actor Thread” with their names. We’ll see how far it goes later. After printing this message we just wrap word count inside ControllerWordCountReply() case class and send it back to controller(using ! ). In this case the reference of Controller Actor is replyTo.
The case _ => is just scala syntax for “if message does not match any above cases”.
Hope you got the idea behind this worker actor. Now let’s move to the controller actor. Don’t rush and let’s examine this piece of code step-by-step.
First thing we can tell is, WordCounterController actor accepts messages of type ControllerCommand that’s why we provide concrete cases for StartCounting and ControllerWordCountReply. Also what may seem strange here is totalWordCount and totalWorkerCount parameters that have default values 0,1 and are passed to apply method. This is purely functional style to maintain state inside our actor. You could have created state variable out of the apply method but let’s do it FP way.
Let’s examine ControllerWordCountReply(count) case, when our actor receives this message it means that worker actor has done its job and returned a word count of some text from the file. What do we do now? We just create newTotalCount variable updated with totalCount + count. Then we just print the updated count.
Finally we return apply(newTotalCount, totalWorkerCount) this last line here is extremely important. We have just returned the behaviour with which the next message should be handled. Same apply method, with updated totalWordCount parameter. Are you getting it? FP is all about immutability. We just removed any need of internal state, and encapsulated the whole state in input parameters. Next apply call will have updated totalWordCount.
StartCounting message is pretty straightforward. We just read lines from the text file. For every line spawn(create) a worker actor that will asynchronously count the word in the line and then get back with the reply message. workerIndex is just the counter for workers, as we need to name workers uniquely. As you can see we just spawn worker actors and send tasks to them. returning apply() method with updated data is same as in previous example.
For complete example visit the link: https://github.com/acho01/akka-blog/blob/main/src/main/scala/Example2.scala
Running Word Counter
First we run word counter for a small text file.
Because of small number of actors, every JVM thread (OS thread) is mapped to each worker. It looks fine and no big performance issue is caused here. But what happens if we run some really big text file with hundreds of lines? Will Akka still create hundreds of OS threads while spawning worker actors? Let’s run and see what happens.
For really big text file, the log is too long and this is just a small snippet. Nevertheless, you can see that more than 400 worker actors are scheduled on 11 threads. This pattern goes in whole log. Akka runtime schedules actors on much small amount of OS threads.
Actor Supervision
Last but not least, concept about actors which I want to discuss is Supervision. If you are coming from traditional OOP background you will love how Akka manages failures. What if I told you that when thread dies, you can resurrect it? Well it’s the default convention for failure management in Actor Model. Every actor supervises its child actors. It means, whenever child actor fails, it’s supervisor’s responsibility to deal with the failure. In this example, if G actor fails, E will have to decide what to do. As supervisor in Akka, actor can resume, restart or stop the failed child.
As our blog is already pretty long I would just ask you to have a look at Fault-Tolerance chapter in akka docs. There are pretty good coding examples explaining this concept.
Summary
For sure there are dozens of other examples and topics to cover around Actor Model and Akka. Unfortunately it’s impossible to cover all that in one try. Hope these 2 blogs will help you to get the fundamental idea of this great technology. Akka provides separate modules for Persistence and Clustering. If you want to code some distributed features using Akka’s magic you should definitely dive deep into Akka docs, which is one of the best structured docs I’ve ever seen. https://doc.akka.io/docs/akka/current/
Hope you enjoyed:)
Let’s connect: https://www.linkedin.com/in/archil-sharashenidze/
All materials used to understand actor model and deliver this blog to you, are collected by me in the process of learning. Checkout my github repo containing different resources about actor model.
https://github.com/acho01/actor-model-guide
Also checkout my akka examples repo which contain all the examples written by me in the process of learning akka.
https://github.com/acho01/actor-study
Part 1 of the blog: Demystifying concurrency using Actors, Let there be Abstraction (Part 1)