In this blog we will learn about:
- How to make use of Akka Libraries.
- Practical use of Actor Model.
- Performance comparison between different approaches.
Topics Covered
- Conventional programming code and it’s time.
- Code for using our own threads(performance for before and after).
- Akka Code and it’s performance.
Requirements
- We are going to use Java 8 for the code.
- Basics of dependency injection.
- IDE, I use IntelliJ.
- This is 2nd part of Akka, if not already done checkout first part here. Highly recommended!!
This will contain mostly examples of code that you can write for the example in previous blog.
So we will try to calculate the number of prime numbers till 100,000,000.
Before we start, please try to code up the solution for the above problem.
Single Thread Way
- We will have a variable to show the number of primes till now and initialise it with 0.
- We will create a function which will tell if a number is prime.
- For each number from 2 till 100,000,000 if the number is prime we will increment the number otherwise continue to next number.
- Print out our variable.
So you can found the code here.
Let’s discuss some performance of the code now.
In my local system when I run the time taken by the code to execute is around 30 seconds. I also wrote a optimised single threaded way in which I reduced the computation time by half, ~15 seconds. You can find the code in the same file and use the unused function.
Note: The time on your local could very much depend on the processing power of your system
We should always try to optimise our code to the fullest, if there’s some scope of improvement in any approach without affecting other parameters, we should definitely do that or at-least propose the optimised approach.
So the result I got is 5761455 primes.
Introduction of Multithreading
- We will use the concept of threads to calculate the number of primes.
- We will try to optimise by increasing the number of threads for the operation of calculating the primes in the range.
- isPrimeOptimised function from the previous code will be used to calculate primes in the range.
- There will be a thread safe result class which will have the number of primes, once a thread successfully calculate the number of primes in the range, it will add in the number of primes.
- A PrimeNumbers class will be implementing Runnable to run the processes in parallel. It will contain the core logic.
So here we are using synchronized keyword to make the class thread safe(we can use different locking techniques as well), it will ensure that only one thread will be able to access the variable numberOfPrimes.
Find the complete code here.
Some Performance Insights:
10 Threads -> 2988 millis
100 Threads -> 2223 millis
1000 Threads -> 2107 millis
10000 Threads -> 2334 millis
5 Threads -> 4175 millis
Not what you had in mind??
So this happens because, the CPU can only run certain number of tasks on your computer(based on number of Cores). Now you are running something on JVM which will try to run some java code along with other process on your computer parallely. Too many threads mean too many tasks for the CPU to handle and now CPU has to do round robin on tasks.
So let’s say you are doing 10000 Threads(let’s ignore the other processes). Then you have to 10000 tasks/16 Cores processes. Alone context switching might take too much time in this.
So imagine this, let’s say there are 4 doors and 10 people or let’s say 20–30 people try to go to the other side of the doors. And now keeping the doors same let’s say people increased to 500. Will the time taken be directly proportional to the increase? Never, it will take much more time handle the traffic and people to decide which side to go and rush will make more adverse affects.
Akka Way
Phew!!! Finally here. Let’s do it together this time.
Dependencies
I am using maven for project management and dependency injection, you can use any other of your choice.
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor-typed_2.13</artifactId>
<version>${akka.version}</version>
</dependency>
We will mainly need this dependencies to work with Akka Framework.
Actors and Behoviours
So in the previous blog we discussed about some actors and it’s behaviour and those actors can also create new actors with their behaviour inside them called child actors.
So let’s create a Business Logic class, let’s call it NumberOfPrimesBehaviour. This class will extend a Akka Library abstract class called AbstractBehavior, and if you see the class AbstractBehavior you will notice that it requires one data type.
Command
So the data type we discussed about in the Behaviours above, we will call it command. Command will be any class which will implement Serializable inteface(or interface extending Serializable to handle multiple cases which will we look in code below) and it will hold the state for any of the messages. Each message will have it’s own Command so we don’t have to worry about thread safety here.
So let’s start with the code!!
createRecieve() Method
When you extend AbstractBehaviour from your class it will ask you to override the createRecieve() method and create a constructor.
To initialise this from some other context we would need an instance of ActorSystem, which takes in the params of Behaviour(Akka Class) and the name. So to make an instance of Behaviour we can make a function called create().
Now any class which wants to make a new Actor Reference:
ActorSystem<NumberOfPrimesBehaviour.Command> numberOfPrimes = ActorSystem.create(NumberOfPrimesBehaviour.create(), "numOfPrimes");
Sending Messages
Now if we just want to send a message which needs to be processed by the behaviour, we can directly call tell(). This tell command takes in a parameter which is a message(obviously it should be final, if you are asking why just think again why it should be final). This message is our state for the message, it will have the required parameters. So we will have 2 implementations of command for our use case:
- When a new request comes in to start our actor behaviour to cumpute the result. InstructionCommand(will contain the start message)
- When the computation is done and we want to return/print out the result. ResultCommand(will contain the result)
numberOfPrimes.tell(new NumberOfPrimesBehaviour.InstructionCommand("start"));
Receiving Messages
So in the above code image we see the overriden method createReceive. All the messages with that ActorRef will go to that method. So the messages needs to be handled in that method.
Our ReceiveBuilder class(gets from newReceiveBuilder) have a method called onMessage which takes in 2 parameters(which we are going to use) type(Command type in our case) and the functional method(a lambda), which just states that if the type matches then proceed the implementation.
return newReceiveBuilder()
.onMessage(InstructionCommand.class, cmd -> {
if ("start".equals(cmd.getMessage())) {
}
return this;
})
.build();
The code for above explanation and return this is just returning the self context of Actor itself as the expected return type of createReceive function.
Child Actors
As in the approach to the problem we discussed that we will send the message to new Actor to calculate the number of primes(to separate the concerns). We need to spawn new Actor Behaviours(child) in Manager to do the computations.
To create new child actors we need to :
ActorRef<NumOfPrimesWorkerBehaviour.Command> actorRef
= getContext().spawn(NumOfPrimesWorkerBehaviour.create(), "worker" + i);
actorRef.tell(new NumOfPrimesWorkerBehaviour.Command("start", i, getContext().getSelf()));
Here I have directly used Command as a class in NumOfPrimesWorkerBehaviour(you can see that I directly pass the params here). Also keep an eye on the third parameter(it is the reference to the current behaviour).
Now one question arises why are doing getContext().spawn() rather than just doing ActorSystem.create()?
The getContext().spawn()
method is a part of the actor context (ActorContext
) and is used to create child actors within the supervision hierarchy of the parent actor. This means that the parent actor is set up to supervise and manage the lifecycle of its child actors. If a child actor crashes, the parent actor can decide how to handle the failure by implementing custom supervision strategies.
On the other hand, ActorSystem.create()
is typically used to create the top-level actor in your Akka system. The top-level actor is not supervised by any other actors within the Akka hierarchy. It is responsible for managing the overall lifecycle of the application and coordinating the creation and termination of actors.
So one task for you….
Just try to make the child behaviour NumOfPrimesWorkerBehaviour
The child will send back the message back to Parent Behaviour to sum up the results. Try to make one more onMessage in the Parent’s behaviour’s createReceive and then print out the final result.
Please find the full code here.
The code will seem to be much more complex but once you get the hang of Akka you will be able to code it up very quickly.
With 10 Actors I am getting the output:
number of primes: 5761455
2870 millis
This is similar to our thread example. But here we have more intuitive code which is extensible to much more use case. We don’t have to worry about the shared data as massages are processed one at a time. We don’t need to use synchronisation which may lead to much adversed performance issues as many programs can try to use/change the same data resource(Making it synchronised is the worst thing we can do).
Conclusion
So we were able to code up our first example of Akka Framework in Java. We got familiar with the very basic Actor, Behaviour and Messaging in Akka. The advantage we have over basic thread handling.(You may also try this over executor service but still we would need some kind of synchronisation techinques)
There are many more use cases which fit into so much more that can be explored.
Please feel free to type in any suggestions/questions in the comments.