Actor Model
Code that accompanies this article can be downloaded here.
In the same time when first object-oriented languages were emerging, another concept inspired by general relativity and quantum mechanics was taking shape – actor model. In general terms, the Actor model was defined 1973 and was developed on a platform of multiple independent processors in a network. Similar to the object-oriented approach, this essentially mathematical model, revolved around the concept of actors. An actor is the smallest structural unit of Actor model, and just like objects, they encapsulate data and behavior. In difference to objects, however, actors communicate with each other exclusively through messages. Messages in actors are processed in a serial manner. According to the full definition of actors, they can do three things:
- Send a finite number of messages to other actors
- Create a finite number of new actors
- Designate the behavior to be used for the next message it receives
Some of the authors are claiming that actors are actually the most stringent form of objects. Let’s not forget that objects in Smalltalk-80 could hold state, and send and receive messages, and that does sound awful like the definition of an actor. But apart from that, we can definitely see that there is a great benefit in this model. Especially in concurrent, parallel processing environments and distributed systems. This is due to the fact that actors can affect each other only using messages, and by that, all locks are eliminated. Also, we can find a use for this concept in rising world of microservices. We can consider that each microservice is, in fact, an actor in its own process.
What is great about this model is that we can apply best object-oriented practices on it. It seems that it is natural to use actors in combination with Single responsibility principle, and make each actor do one thing (again pushing us to the concept of microservices). Also, we should notice the importance of messages. They are no longer just carriers of data, but also in a more abstract manner, carriers of behavior. What an actor will do is depending on what message it received. This brings us to the fact that in actor systems, messages should be kept immutable, so they don’t change in the middle of processing and by that affect behavior of the system. Also, this way, race conditions would be minimal.
Another benefit of these systems is that they are inherently asynchronous. This can be considered a limitation too because the synchronous behavior is harder to achieve.
Akka.NET
Akka is a toolkit which allows us to create an actor system in an efficient and simple way in the .NET environment.
Configuration
To start with Akka.Net, you should first install the package in your project, using Package Manager Console:
Install-Package Akka
Also, to avoid warning about deprecated serialization, install Hyperion
package too:
Install-Package Akka.Serialization.Hyperion -pre
and add this to your App.config file:
<configSections>
<section name="akka"
type="Akka.Configuration.Hocon.AkkaConfigurationSection, Akka" />
</configSections>
<akka>
<hocon>
<![CDATA
[akka
{actor
{serializers
{hyperion = "Akka.Serialization.HyperionSerializer,
Akka.Serialization.Hyperion"
}
serialization-bindings {
"System.Object" = hyperion
}
}
]]>
</hocon>
</akka>
Simple Use Case
When working with actor system, the first thing we need to define is a message type which actor will react to.
public class Message
{
public string Text { get; private set; }
public Message(string text)
{
Text = text;
}
}
Once that is defined, actor class can be created, by implementing abstract ReceiveActor
class:
public class MessageProcessor : ReceiveActor
{
public MessageProcessor()
{
Receive<Message>(message => Console.WriteLine
("Received a message with text: {0}", message.Text));
}
}
And consume that actor like this:
public class Program
{
static void Main(string[] arg)
{
var system = ActorSystem.Create("ActorSystem");
var actor = system.ActorOf<MessageProcessor>("actor");
actor.Tell(new Message("This is first message!"));
Console.ReadLine();
}
}
How About Something More Complicated
Ok, that was one easy example to get you started on how Akka works in general. But let’s consider something a little bit more complicated. Let’s make the system that will collect data about how long each reader was reading the article. The system will look something like this:
It will contain the following actors:
- Blog Actor – Drives the whole system and receives messages from the simulated frontend. It will delegate messages to the rest of the system.
- Reporting Actor – Gathers data from users and blog, and displays data to console.
- Users Actor – Parent of individual user actors, used to delegate messages to correct user.
- User Actor – Calculates how much time user has spent on reading the certain article and forwards that information to Reporting Actor.
In order to drive the whole system, there will be three types of messages:
StartedReadingMessage
– This message will indicate that user started reading the article. StopedReadingMessage
– This message will indicate that user stopped reading the article. ReportingMessage
– This message will be sent from User Actors
Since they all carry similar information, there is base Message
class. Here is its implementation:
public abstract class Message
{
public string User { get; private set; }
public string Article { get; private set; }
public Message(string user, string article)
{
User = user;
Article = article;
}
}
We can see that base message contains information about the user and about the article. Rest of the messages are used for containing information about action which is performed:
public class StartedReadingMessage : Message
{
public StartedReadingMessage(string user, string article)
: base(user, article) {}
}
class StopedReadingMessage : Message
{
public StopedReadingMessage(string user, string article)
: base (user, article) {}
}
public class ReportMessage : Message
{
public long Milliseconds { get; private set; }
public ReportMessage(string user, string article, long milliseconds)
: base (user, article)
{
Milliseconds = milliseconds;
}
}
The main program drives this simulation by initializing system as a whole and sending messages to the Blog Actor:
static void Main(string[] args)
{
ActorSystem system = ActorSystem.Create("rubikscode");
IActorRef blogActor = system.ActorOf(Props.Create(typeof(BlogActor)), "blog");
blogActor.Tell(new StartedReadingMessage
("NapoleonHill", "Tuples in .NET world and C# 7.0 improvements"));
Thread.Sleep(1000);
blogActor.Tell(new StartedReadingMessage
("VictorPelevin", "How to use “
Art of War†to be better Software Craftsman"));
Thread.Sleep(1000);
blogActor.Tell(new StopedReadingMessage
("NapoleonHill", "Tuples in .NET world and C# 7.0 improvements"));
Thread.Sleep(500);
blogActor.Tell(new StopedReadingMessage
("VictorPelevin", "How to use
“Art of War†to be better Software Craftsman"));
Console.ReadLine();
}
As mentioned before, Blog Actor delegates messages to the rest of the actors. It is also in charge of creating Users Actor and Reporting Actor. You may notice the use of the Context
property of the actor, which is used for creating child actors. Also, there is use of Props configuration class, which specify options for the creation of actors.
public class BlogActor : ReceiveActor
{
private IActorRef _users;
private IActorRef _reporting;
public BlogActor()
{
_users = Context.ActorOf(Props.Create(typeof(UsersActor)), "users");
_reporting = Context.ActorOf(Props.Create(typeof(ReportActor)), "reporting");
Receive<Message>(message => {
_users.Forward(message);
_reporting.Forward(message);
});
}
}
Users Actor
caches information about users, and routes messages to each individual User Actor.
public class UsersActor : ReceiveActor
{
private Dictionary<string, IActorRef> _users;
public UsersActor()
{
_users = new Dictionary<string, IActorRef>();
Receive<StartedReadingMessage>(message => ReceivedStartMessage(message));
Receive<StopedReadingMessage>(message => ReceivedStopMessage(message));
}
private void ReceivedStartMessage(StartedReadingMessage message)
{
IActorRef userActor;
if(!_users.TryGetValue(message.User, out userActor))
{
userActor = Context.ActorOf(Props.Create(typeof(UserActor)), message.User);
_users.Add(message.User, userActor);
}
userActor.Tell(message);
}
private void ReceivedStopMessage(StopedReadingMessage message)
{
IActorRef userActor;
if (!_users.TryGetValue(message.User, out userActor))
{
throw new InvalidOperationException("User doesn't exists!");
}
userActor.Tell(message);
}
}
Implementation of User Actor goes as follows:
public class UserActor : ReceiveActor
{
private Stopwatch _stopwatch;
private bool _isAlreadyReading;
public UserActor()
{
_stopwatch = new Stopwatch();
Receive<StartedReadingMessage>(message => ReceivedStartMessage(message));
Receive<StopedReadingMessage>(message => ReceivedStopMessage(message));
}
private void ReceivedStartMessage(StartedReadingMessage message)
{
if (_isAlreadyReading)
throw new InvalidOperationException
("User is already reading another article!");
_stopwatch.Start();
_isAlreadyReading = true;
}
private void ReceivedStopMessage(StopedReadingMessage message)
{
if (!_isAlreadyReading)
throw new InvalidOperationException
("User was not reading any article!");
_stopwatch.Stop();
_isAlreadyReading = false;
Context.ActorSelection("../../reporting").Tell
(new ReportMessage
(message.User, message.Article, _stopwatch.ElapsedMilliseconds));
_stopwatch.Reset();
}
}
And last, but not the least here is the implementation of Reporting Agent. It gets data from each individual User Actor, and from Blog Actor, and calculates time spent on each blog post, and the number of views on each blog post.
public class ReportActor : ReceiveActor
{
private Dictionary<string, long> _articleTimeSpent;
private Dictionary<string, int> _articleViews;
public ReportActor()
{
_articleTimeSpent = new Dictionary<string, long>();
_articleViews = new Dictionary<string, int>();
Receive<ReportMessage>(message => ReceivedReportMessage(message));
Receive<StartedReadingMessage>(message => IncreaseViewCounter(message));
}
private void ReceivedReportMessage(ReportMessage message)
{
long time;
if (_articleTimeSpent.TryGetValue(message.Article, out time))
time += message.Milliseconds;
else
_articleTimeSpent.Add(message.Article, message.Milliseconds);
Console.WriteLine("******************************************************");
Console.WriteLine("User {0} was reading article {1} for
{2} milliseconds.", message.User, message.Article, message.Milliseconds);
Console.WriteLine("Aricle {0} was read for {1} milliseconds
in total.", message.Article, _articleTimeSpent[message.Article]);
Console.WriteLine("******************************************************\n");
}
private void IncreaseViewCounter(StartedReadingMessage message)
{
int count;
if (_articleViews.TryGetValue(message.Article, out count))
_articleViews[message.Article]++;
else
_articleViews.Add(message.Article, 1);
Console.WriteLine("******************************************************");
Console.WriteLine("Article {0} has
{1} views", message.Article, _articleViews[message.Article]);
Console.WriteLine("******************************************************\n");
}
}
This is how the result of this simulation looks like:
Conclusion
Actor Model gives us a different way of solving problems. Once you get into the message-driven mindset, you’ll find the Actor Model to be of great value when it comes to designing large-scale, service-oriented systems. On the other side, Akka.NET gives us a framework in which we can create these systems fairly easy. Here, we covered just basic uses of Akka.NET, but it has many more features that can help you.
If you need more information about the Actor model, I recommend this video. For more information about Akka.NET, you can visit their official site.
This work is licensed under a Creative Commons Attribution 4.0 International License.