For writing programs that perform parallel computing, threads are widely used. Given that the streams allow for quite flexible organization of parrallism in programs, they have a number of drawbacks. The fact is that threads share memory among themselves. This means that it is very easy to neglect the integrity of the program. This can be overcome with the help of locks that allow some code to get exclusive access to the shared resource. However, the locks themselves, besides the fact that they need to be remembered to be affixed, cause problems in turn. One of the worst problems is the ability to spawn a deadlock. However, even without this, writing a really well-functioning multi-threaded program turns into jewelry work.
But threads have alternatives. From known to me - model of actors (actors) and software transaction memory. The hero of this article, as is clear from the title, are the first. However, on STM there are a lot of articles on the Internet that will satisfy your curiosity.
What is the model of actors
')
The Internet is full of references to the model of actors. Therefore, I will only write about what actors are, without going into history, application, usage patterns.
So, actors are objects that:
- do not share states with each other;
- interact with each other only through the sending of asynchronous messages;
- the actor does not process any two messages simultaneously; instead, the actor collects incoming messages in a queue and processes them sequentially.
Actors are very reminiscent of various message queue systems, such as JMS, ActiveMQ or MSMQ. In addition, in many GUI frameworks, controls, like actors, interact with each other using asynchronous messages.
Distributed computing
Why is it necessary in parallel computing to represent that the calculations are spinning on one physical machine? And if we have a cluster? For high-load projects this is especially important, since sooner or later they rest against the boundaries of one car. The thread model in this case does not fit at all, since threads have shared memory. If you try to implement something similar in the case of a distributed system, there will be a problem with the synchronization of data between the nodes, because the threads running on different nodes should seem to have shared memory. But the actors interact with each other only through asynchronous messages, which are well transmitted over the network.
Actors have another advantage for distributed computing. Because the actor does not share its state with anyone; this very state is easy to serialize and transfer between nodes. On the other hand, the actor is not obliged to respond to messages instantly, because all messages are delivered asynchronously, so it will be normal for the actor if he spends some time on the serialization / deserialization of the state, postponing the message processing for a while.
Existing implementations
Traditionally, the model of actors is loved among Erlang and Scala programmers, where actors are supported natively. There are libraries for various languages, including Java, the most famous of which is Akka. On Habré there is an
overview of this library.
However, even the presence of Akka did not stop me from wanting to make my own library to support the actors. Why do I need it, I will explain below. In the meantime, I'll show you how actors work in my library.
nop.actors
I implemented support of actors in the
framework . The mechanism resembles typed actors from Akka, and there is only asynchronous message passing.
How to describe an actor? To do this, you need to describe the interface, i.e. the set of messages processed by the actor, and the implementation, i.e. proper processing of messages. If we draw parallels with the languages ​​where pattern matching is supported, then the interface description is equivalent to an algebraic type declaration, and the implementation is the pattern matching itself. Here is an example of the description of the actor:
@Actor public interface Pingable { void ping(String token, Pinger pinger); }
@Actor public interface Pinger { void pong(String token); }
public class DefaultPingable implements Pingable { @Override public void ping(String token, Pinger pinger) { System.out.println("Pinging with: " + token); pinger.pong(token); } }
This is how it will be used:
Pingable pingable = Actors.wrap(Pingable.class, new DefaultPingable()); pingable.ping("hello", new Pinger() { @Override public void pong(String a) { System.out.println("Ping received: " + a); } });
There are certain restrictions on the data passed in the arguments. Informally, they can be understood in such a way that only primitives, POJOs, other actors and lists can be transmitted. Formally, this is described as follows. Let S be the set of all types that are transmitted in messages. Then:
- primitive types and wrapper classes are included in S;
- any interface marked with Actor annotation is included in S;
- any enum is in S;
- List <T>, Set <T>, T [] are included in S if T is included in S;
- Any class consisting of private fields and a pair of accessor methods for each of them is included in S if the type of each of the fields is in S.
Now let's see how to make actors accessible remotely via the HTTP protocol. For this, there is a mechanism integrated into nop and using the framework tools. To make an actor accessible remotely, you must call the exportActor method on an ActorManager object. ActorManager can be obtained through dependency injection:
public class PingController { private ActorManager actorManager; @Injected public PingController(ActorManager actorManager) { this.actorManager = actorManager; } public Content pingDemo() { ActorInfo fooInfo = actorManager.exportActor(new DefaultPingable());
Of course, it is not necessary to use the features provided by the nop framework as a whole. If you do not need to drag all the dependencies, it is enough to do with the HttpActorDispatcher class alone, placing it in your servlet.
Actors on the browser side
The above properties of the actors suggest that the actors are just perfect for web applications where you need to actively exchange data between the browser and the web server in both directions. But this requires support for the actor model in JavaScript. And she is in nop.actors!
If you take the actors only within the browser itself, then everything is simple with them. First, the constructor of the actor class needs to be wrapped in the actor call. Secondly, to make an instance of a class an actor, you need to wrap it with the actor method. Here is what the Pinger actor would look like:
DefaultPinger = actor(function(elem) { this.elem = elem; }); DefaultPinger.prototype.pong = function(token) { var messageElem = document.createElement("div"); messageElem.textContent = token; this.elem.appendChild(messageElem); }
var pinger = actor(new DefaultPinger(document.getElementById("pingResult"))); pinger.pong("hello");
If it is necessary to ensure the interaction of the actors on the browser side with the actors on the web server side, then the JavaScript interface should be described. For our example, the description will look like this:
Pingable = {}; Pinger = {}; Pingable.ping = ["value", actorRef(Pinger)]; Pinger.pong = ["value"];
The general principle here is. The protocol by which you need to communicate with the actor is described as an object. The properties of the object correspond to the messages processed by the actors. The property value should always be an array listing the types of arguments passed with the message. The framework understands the following types of arguments:
- the string “value” matches any primitive type or enum;
- actorRef (A), where A is the actor description;
- an array consisting of one element A indicates that the argument is a collection with an element of type A;
- an arbitrary object means POJO, and the values ​​of the properties of the object indicate the types of the POJO properties.
The class ActorRemoting is responsible for working with remote actors. Here is an example of initializing and using the class:
var remoting = new ActorRemoting("/actors/" + sessionId); var pingable = remoting.importActor(Pingable, nodeId, actorId); remoting.start(pregable.ping("hello", pinger);
Of course, the server should, first, somehow transfer sessionId, nodeId and actorId to the browser. For example, it can do this when generating a page by adding JavaScript code to it that initializes these variables.
Complete example
Above, I talked about the implementation of the model of actors in nop, both on the server side and on the browser side. However, I still kept silent about how to tie all this together in one small application. So, we also need the following classes:
@Route(prefix = "pingpong") public interface PingRoute { @RoutePattern("/") String main(); }
@RouteBinding(PingRoute.class) public class PingController extends AbstractController { private ActorManager actorManager; @Injected public PingController(ActorManager actorManager) { this.actorManager = actorManager; } public Content main() { ActorInfo pingInfo = actorManager.exportActor(Pingable.class, new DefaultPingable()); return html(createView(PingView.class).setActorInfo(pingInfo)); } }
public class PingView extends Template { PingView setActorInfo(ActorInfo actorInfo); }
@ModuleRequires(modules = ActorsModule.class) public class PingModule extends AbstractModule { @Override public void load() { app.loadPackage(PingModule.class.getPackage().getName()); } }
In addition, you need to add a PingView.xml template with the following content:
<?xml version="1.0" encoding="UTF-8"?> <t:template xmlns:t="http://nop.org/schemas/templating/core"> <t:head> <t:parameter name="actorInfo"/> <t:service name="actorsRoute" class="org.nop.actors.ActorsRoute"/> </t:head> <t:body> <html> <head> <title>A simple actors example</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <script src="${actorsRoute.resource('actors.js')}" type="text/javascript"/> </head> <body> <div> <input type="text" id="argument"/> <button id="pingButton" type="button">Ping</button> </div> <div id="pingResult"/> <script> Pingable = {}; Pinger = {}; Pingable.ping = ["value", actorRef(Pinger)]; Pinger.pong = ["value"]; DefaultPinger = actor(function(elem) { this.elem = elem; }); DefaultPinger.prototype.pong = function(token) { var messageElem = document.createElement("div"); messageElem.textContent = token; this.elem.appendChild(messageElem); } var pinger = actor(new DefaultPinger( document.getElementById("pingResult"))); var remoting = new ActorRemoting("/actors/${actorInfo.sessionId}"); var pingable = remoting.importActor(Pingable, ${actorInfo.nodeId}, ${actorInfo.actorId}); remoting.start(); var argumentElem = document.getElementById("argument"); document.getElementById("pingButton").onclick = function() { pingable.ping(argumentElem.value, pinger); } </script> </body> </html> </t:body> </t:template>
An example ready for launch can be downloaded
here .
Chess on the actors
Here, it seems, everything is obvious: there are three actors in the game: two players and one playing field. The last actor checks the players' moves and notifies them about each other's moves. So one could write about such interfaces:
@Actor public interface Board { void registerPlayer(Player player, PieceColor color); void move(Player player, BoardLocation source, BoardLocation destination); }
@Actor public interface Player { void moveRejected(); void moved(BoardLocation source, BoardLocation destination); }
The Board implementation obviously sends the message MoveRejected back to the player if he made a wrong move or went at the wrong time. The message moved is sent to both players. Walked - in confirmation of the move, the opponent - as a notification of the opponent's progress.
Here only this interface is made very naive. The player making the move himself indicates on whose behalf the move is made. An attacker can use this insidiously. In addition, if, for some reason, one of the player actors “fell”, then it is not possible to restore the state of the game. So, we write the following interfaces to overcome these disadvantages:
@Actor public interface Board { void authorizePlayer(PlayerObserver observer, String key); }
@Actor public interface Player { void move(BoardLocation source, BoardLocation destination, PieceType promotedType); }
@Actor public interface PlayerObserver { void authorizationAccepted(PieceColor color, Player player); void authorizationRejected(); void moveRejected(); void boardStateChanged(BoardState state); void moved(BoardLocation source, BoardLocation destination); }
So, that's what happened. Now the player is hidden behind the PlayerObserver interface. And the playing field is represented by one Board and one Player. Player - this is something like the angle of the playing field, available to one particular player. Entering the game, the actor player tells the playing field a password and passes the link to himself. From the playing field, he receives confirmation in the form of a message authorizationAccepted. In addition, by connecting to the game, the actor receives the state of the playing field at the moment as a message boardStateChanged.
Please note that both the Player and Board actor must share state with each other. So it really is, nop supports it. We can say that this is one actor that is visible from different angles. Here’s what the authorizePlayer message handling actually looks like:
@Override public void authorizePlayer(PlayerObserver observer, String key) { boolean matches = false; for (PlayerImpl player : players.values()) { if (player.token.equals(key)) { observer.authorizationAccepted(player.color, player); sendFullState(observer); player.setPlayerObserver(observer); updateObserverList(); matches = true; break; } } if (!matches) { observer.authorizationRejected(); } } private void sendFullState(PlayerObserver observer) { BoardState state = new BoardState();
Here the player is passed to the observer during message processing by the board. Because Player was clearly not done by actor, the framework will automatically make it as such and at the same time combine with the board. In fact, to create a new actor that does not have a common state with others, it is necessary to explicitly make it an actor using Actors.wrap.
The full code of Board / Player actors can be found
here .
The JavaScript part requires an actor interface description. Here's what it looks like:
Board = {}; Player = {}; PlayerObserver = {}; BoardLocation = { row : "value", column : "value" }; PieceState = { type : "value", color : "value", location : BoardLocation }; Move = { source : BoardLocation, destination : BoardLocation, piece : "value", capturedPiece : "value" }; BoardState = { moves : [Move], pieces : [PieceState] }; Board.authorizePlayer = [actorRef(PlayerObserver), "value"]; Player.move = [BoardLocation, BoardLocation]; PlayerObserver.authorizationAccepted = ["value", actorRef(Player)]; PlayerObserver.authorizationRejected = []; PlayerObserver.moveRejected = []; PlayerObserver.boardStateChanged = [BoardState]; PlayerObserver.moved = [BoardLocation, BoardLocation];
The PlayerObserver implementation simply redraws the page when messages arrive from the server and sends messages to the server when the player moves the piece.
The full implementation of the browser actor is available
here .
The chess example is included in the nop distribution as a demonstration application. The application code is located in the / demo / chess folder. I also raised the finished
service .
Benefits
In addition to achieving transparent messaging, nop.actors can also do the following things.
First, the framework is fully modular. There is an implementation of the mechanism of actors within the process. There is an implementation of remote actors, made on top of the actors within the process, and this implementation can use any transport.
Secondly, asynchronous processing of long-poll requests using servlets version 3.0. If 1000 clients are connected to the server, this will not mean that it will create 1000 threads that do nothing.
Thirdly, the system automatically unloads the do-nothing actors onto the disk, freeing memory. At the same time, the life of any actor is potentially infinite.