By Stepan Mitkin
This document describes an attempt to use the DRAKON visual language for actor-based programming. The goal is to see how practical DRAKON is in the context of actors, state machines and message passing.
What has been done:
Each actor in the applications is a DRAKON diagram. A C# class is generated from the diagram and included in a Visual Studio project.
Several often used concurrency patterns have been tested: IO-bound operations, CPU-bound operations and GUI management.
Get the source code on GitHub
Actors have proven to be a reliable foundation for building concurrent systems. The actor model divides the program into a sequential and a concurrent part.
Concurrent code is extremely hard to get right. Fortunately, the actor model outsources this part to the framework.
Actors themselves consist of sequential code. Sequential code is easier to write. It is less scary to delegate this part of work to application developers.
Extracting concurrent code from the application and concentrating it in a well tested module is the main benefit of the actor model.
Actors solve the problem of synchronisation of access to shared data in an elegant way. There is no shared data. Each piece of data belongs to some actor.
Of course, it is still possible to create a deadlock or starvation. Programming is never safe. But the actor model reduces these dangers considerably.
Another benefit of actors is better encapsulation. In object-oriented programming, there is always a risk that someone from the outside will modify the object in the middle of that object's method. It can happen even if there is only one thread in the program!
With actors, this is not possible. No one can access the actor's state except for the actor itself. Actor code has full control over what and when gets changed inside the actor.
State machine and actor are similar concepts. Both react to incoming messages. What is the difference between them?
Actors imply the presence of an environment that carries out message passing. Actors communicate with each other by sending messages through this environment.
A state machine can switch between several modes of operation. The actual reaction of the state machine to an incoming message depends on both the message itself and the state. After responding to the message, the state machine can shift to another state. This idea of switching states is convenient because it creates an explicit link between the past of the machine and the decision what to do next. That makes state machines a good tool for implementing behaviour.
It is clear that the difference between state machine and actor is not crucial. They are not mutually exclusive. Actors can and should be designed as state machines where appropriate. Combining the strengths of both concepts is straightforward.
There is no surprise that OTP (one of the most well-known actor frameworks) has a special template for state machines as actors. See documentation on gen_fsm.
The DRAKON visual language has a unique facility called silhouette. Silhouette breaks up a single diagram into several regions without totally separating them. These regions are called branches. Usually, branches divide the algorithm into logical parts.
Another way to use branches is to define states of a state machine. Each branch describes one state. The branch header shows the name of the state. The body of the branch contains reactions to messages in this particular state. The bottom of the branch indicates the next state or states. It is possible to switch to any of the states of the machine or remain in the same state. Switching to the last state will execute the CleanUp code and destroy the machine.
The good thing here is that DRAKON shows both the state transitions and the logic of the machine. DRAKON combines a state machine diagram with decision trees.
Two applications were created to test the concept: ActorHttp and ActorGui. ActorHttp puts DRAKON actors to work in a server environment. In ActorGui, DRAKON actors perform typical tasks of an application with a graphical user interface. Both applications were written in C# and run under the .NET framework.
The actor runtime used in the test applications requires that the actor is a class that implements the IActor interface. IActor has two methods: OnMessage and CleanUp.
Both OnMessage and CleanUp are guaranteed to run in the same thread.
Actors should never hold references to each other. Integer actor ids should be used instead. Actors must communicate only by sending messages through the runtime. Sending a message requires an actor id.
DRAKON Editor creates an actor class from a silhouette diagram if the text in the Formal Parameters icon is "state machine".
The silhouette branches become states. The exit branch becomes the destructor.
The first icon of each state branch must be a "Select" icon with text "receive". The case icons must contain the codes of messages that are important for this specific state. Receiving an unexpected message does not do anything.
Shifting to the exit branch results in removing the actor from the runtime and invoking its destructor.
ActorHttp is a simple web server that publishes static files from a folder on the hard drive. It is based on the HttpListener .NET class.
There is one instance of HttpListener and one PumpManager actor. The HttpListener instance accepts HTTP requests and sends them to the PumpManager. The PumpManager creates worker actors and hands over the requests to them. The worker actors are randomly allocated in one of the threads hosted by the actor runtime. The threads themselves are created during the application startup.
The main actor of the HTTP server is PumpManager. PumpManager is a state machine with only one state. There is only one PumpManager in the server. The PumpManager receives HTTP requests, creates actors to handle them and closes the HTTP responses when the actors finish their work. The PumpManager also throttles incoming requests if a specified limit of simultaneously processed requests is reached.
The job of a StreamPump is to read data from an input stream and then write it to an output stream. For HTTP requests, the input stream is a disc file and the output stream is the HTTP response stream. StreamPump acts as an intermediary for this pair of streams. Both reading and writing is carried out asynchronously. StreamPump takes processor time only when an important event occurs with any of the streams. Such events include a successful completion of the IO-operation, an error or a cancellation.
Many StreamPumps can run inside one thread. This makes them very scalable. StreamPumps can handle lots of simultaneous HTTP requests.
StreamPump is a state machine with several states. Each of its states corresponds to a stage in the lifecycle of the request. Since both reading and writing are done at the same time, two buffers must be used.
The IO operations are done in small chunks. When one chunk is complete, the buffers are swapped and the next chunk begins.
The first state is JustRead. Only reading is performed because there is nothing to send yet. After the first chunk is read into the buffer, StreamPump swaps the buffers, starts sending the previously read data and begins a new reading operation. Waiting for two operations is done in the ReadSend state. Depending on which operation completes first, StreamPump shifts to JustSend or again to JustRead state. When all file has been read, StreamPump switches to the SendRemaining state and sends whatever is left to send. Here are a few use cases of StreamPump operation.
The rightmost branch is the destructor. It is invoked when the actor is removed from the runtime. StreamPump's destructor closes the streams and notifies the manager actor that the operation has completed. The destructor is called regardless of the outcome of the IO operations.
ActorGui is a WPF application that can download resources from the internet and compute prime numbers. Both downloading and computation are done in the background and do not block the UI. Any of these tasks can be cancelled at any time.
In this test application, actors perform three types of tasks:
MainWindowLogic actor works as a code-behind. It controls the GUI widgets and manages the background tasks.
The actor runtime creates the MainWindowLogic actor in the GUI thread. Therefore, there is no need to worry about updating the GUI widgets in the right thread.
PrimeCalculator actor runs prime numbers computation. Since it is a CPU-intensive task, the runtime allocates a dedicated OS thread for this actor. The computation is done in chunks. For each chunk, the runtime sends a pulse message to the PrimeCalculator actor. The PrimeCalculator runs a fixed number of iterations and returns control to the runtime. This is a cooperative multitasking of sorts.
It is possible to destroy the actor between pulse messages and thus cancel the computation. After the computation is completed, the PrimeCalculator sends a message to MainWindowLogic. The latter then stops the "working" animation and reverts the GUI to the "ready" state.
The usual ways to report errors in .NET are exceptions and return values. Neither work in a concurrent message-passing environment. Actors return computation results and notify of errors by sending messages. Therefore, when waiting for an operation to complete, the caller actor should be prepared to receive an error message.
The runtime has a special mechanism for making calls and waiting for results. This mechanism ensures that the caller never receives results of calls made by someone else.
Certain operations can time out or get cancelled. The caller actors should account for that.
It is the callee's responsibility to eventually send a reply. If the operation completes successfully, the reply should contain the result of the operation. If the operation throws an exception, the callee must catch it and send it back in an error message.
Unhandled exceptions pose a problem. The runtime will of course catch them and the application will not crash. The risk is that the actor which threw the exception may be left in an inconsistent state.
Erlang would simply kill the failed actor (process) and all the linked actors. Whereas the runtime used in the test applications invokes a special callback. The callback can examine the damaged actor, repair it or replace it with a new one. The callback is guaranteed to run in the same thread that the actor runs in.
Actors give most of their value when designed as state machines. And DRAKON gets along well with state machines:
A good compromise is reached. On one hand, the whole actor is on the same visual scene. This is crucial for readability. Yet on the other hand, algorithms that belong to different states are explicitly divided. Continuations are not artificially glued to the code they follow. Showing a visual separation between things that are separated by time is a great aid to understanding programs.
DRAKON fits in the actor model surprisingly well.
29 December 2014
Contact: drakon.editor@gmail.com