|
|
Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs
Page 2 of 3
Listing 1 defines a Scala actor. The act() method is part of the contract of the Actor trait and is analogous to the function executed by an actor spawned in Erlang.
class Player() extends Actor {
def act() {
// message handling loop
}
}
Before looking in more detail at the actors, we first need to define some messages, as shown in Listing 2. In Scala, a class is a type definition, and you can create instances with new just as in Java. You can also define a type as an object, which means it is always a true singleton instance. To do pattern matching, Scala has a concept of case classes and case objects. Case classes are defined with a special case modifier that causes the compiler to add some extra code to the class. Actors perform pattern matching on messages received
from their inboxes, so it is common to use case classes to define messages.
abstract case class Move
case object Rock extends Move
case object Paper extends Move
case object Scissors extends Move
case object Start
case class Play(sender:Actor)
case class Throw(player:Actor, move:Move)
Listing 2 defines an abstract Move class and three singleton implementations of that class representing rock, paper, and scissors. In Java you might use an
enum in this case (as you'll see in later examples). Listing 2 also defines a singleton Start message and classes for Play and Throw message classes.
The Coordinator actor, defined in Listing 3, first creates the two Player actors and starts them. In Erlang, actors frequently loop by making a tail-recursive call back to the original function.
In Scala, the loop { } construct provides this looping behavior.
Listing 3. The Coordinator actor
class Coordinator() extends Actor {
def act() {
val player1 = new Player().start
val player2 = new Player().start
loop {
react {
case Start => {
player1 ! Play(self)
player2 ! Play(self)
}
case Throw(playerA, throwA) => {
react {
case Throw(playerB, throwB) => {
announce(playerA, throwA, playerB, throwB)
self ! Start
}
}
}
}
}
}
Within the loop, you will typically see either react { } or receive { }. The react behavior, similar to Erlang, provides a lightweight actor that is not bound to a thread. The receive behavior creates an actor that is actually bound to a java.lang.Thread (and ultimately to a kernel thread). Which model you choose will depend on your application. It's nice to have the flexibility
to decide which actors must be bound to real threads and which can be lightweight.
Every actor has an internal mailbox where messages are received from other actors and consumed within the actor. Messages
are matched using pattern matching, one of Scala's most powerful features. In the Coordinator actor, we see our case classes being used during message matching. Messages are sent asynchronously with the ! operator, just as in Erlang: actor ! message. A few other operators are also provided:
!? sends a message and waits for a reply (synchronous).
!! sends a message and returns a Future for retrieving the reply.
The Scala support for actors is quite strong, building firmly on the Erlang model. Writing actors code is a natural part of writing Scala code, and the Scala library has full support not just for actor definition, pattern matching, and flexible message send, but also for many of the Erlang-style error-handling primitives. An implementation of the Erlang OTP Supervisor functionality and a distributed version using Terracotta also exist.
GParallelizer is a relatively new library that provides actor support in Groovy. It benefits from Groovy's support for dynamic typing and closures to make writing actors straightforward. And it takes full
advantage of the upcoming Java 7 fork/join and ParallelArray functionality to ease the writing of concurrent code.
GParallelizer also supports both thread-bound and event-driven actors. Listing 4 shows how we can create an anonymous event-driven actor with GParallelizer for our Coordinator actor.
def coordinator = PooledActors.actor {
loop {
react {
player1.send("play")
player2.send("play")
react {msg1 ->
react {msg2 ->
announce(msg1[0], msg1[1], msg2[0], msg2[1])
send("start")
}
}
}
}
}.start()
You'll notice that the syntax is (intentionally) very similar to the Scala example. The major difference here is that instead
of !, we must execute the send() method, and we must roll our own pattern matching with standard Groovy control constructs. (Scala's high-quality pattern
matching is one of its strongest features, so this should not be a surprising difference.)
GParallelizer's extremely lightweight style of actor creation doesn't even require a class here, because you can define the
actor anonymously and directly assign it to the coordinator variable.
GParallelizer is also unique in its support for simplifying concurrency code based on Java Executors and the JSR 166y ParallelArray library. This doesn't really have anything to do with actors, but its nice to see a variety of concurrency approaches supported.
Now we move on to the Java libraries. All of them have a fundamental obstacle to overcome. Scala and Groovy can create lightweight actors by relying on scheduling over a thread pool of actors defined by partial functions or closures. In Java, these techniques don't exist as part of the language, so most of the Java actor libraries rely on bytecode modification either at compile time or runtime.
Kilim is an actor framework that relies on the two concepts of Task and Mailbox. To create an actor, your class should extend Task, but an actor does not have a mailbox created as part of the Task. Instead, you must create and provide Mailboxes between actors as needed by the application. This is a more flexible model, but for simple patterns it feels like more
effort is required.
Mailboxes use generics, and you must define messages passed through mailboxes as classes. Listing 5 illustrates the Player actor in Kilim. It must be initialized with its inbox. The Coordinator's mailbox is passed inside the PlayMessage for a reply.
public class Player extends Task {
private static final Random RANDOM = new Random();
private final String name;
private final Mailbox<PlayMessage> inbox;
public Player(String name, Mailbox<PlayMessage> inbox) {
this.name = name;
this.inbox = inbox;
}
public void execute() throws Pausable {
while(true) {
PlayMessage message = inbox.get();
message.getResponseMailbox().putnb(new ThrowMessage(name, randomMove()));
}
}
private Move randomMove() {
return Move.values()[RANDOM.nextInt(Move.values().length)];
}
}
In older versions of Kilim, a @pausable annotation was used to indicate on the execute() method that the compile-time weaver should do it's magic. In the latest version, that marker has been replaced with the throws Pausable marker on execute().
I found it was easy to write code with Kilim, but it was more complicated to integrate into my tool chain because of the need to run a compile-time weaver. That has also been my experience when I've dealt with other tools that required compile-time modification.
ActorFoundry is a different take on actors in Java. It piggybacks on Kilim by reusing the Kilim compile-time weaver. However, it provides
a different API and a different runtime execution environment. The same Player example from Listing 5 is reworked in Listing 6 for the ActorFoundry.
public class Player extends Actor {
// etc
@message public void play(ActorName coordinator) {
send(coordinator, "playerThrows", name, randomMove());
}
}
In ActorFoundry, each allowed incoming message is defined by a method annotated by @message. There is no receive loop or pattern matching; instead, ActorFoundry just has messages. Messages are sent to an actor (represented
by the ActorName class) using the send() method.
The send() method takes the actor, the message name, and the set of arguments to pass as the message. Being able to use methods directly
and pass multiple arguments actually removes the need to define message classes in most cases, which can simplify the code
somewhat compared to other options. However, needing to specify method names as strings is a strong code smell indicating
a dependence on reflection and hampering the ability to do automated refactoring.
ActorFoundry does both code generation and weaving with Kilim's weaver, which makes for an even more complicated development process than Kilim. I got things working by copying the Ant configuration from the ActorFoundry project and modifying paths as appropriate.
ActorFoundry provides a full runtime environment you must populate with your actor definitions. When you start ActorFoundry, you specify the initial actor message that kicks off the system; after that, everything is done through actors. Thus, you must allow ActorFoundry to structure the execution of your entire application. The combination of the complex compile-time tooling and the surrender of your control of runtime startup and integration require a deep commitment to the ActorFoundry model.
Another difference with ActorFoundry is that it uses a license that is fairly open but only for noncommercial and educational uses.
Actors Guild is a Java actor framework that relies on runtime bytecode modification with the ASM library and thus is easier to integrate into your project than Kilim or ActorFoundry.
Similar to ActorFoundry, the Actors Guild framework relies on annotated methods for each possible incoming actor message.
Each message method must return an AsyncResult parameterized with a reply message (which can be Void for no reply). The Actors Guild Actor base class provides methods such as result(), which can wrap your return value automatically.
Listing 7 shows a play() method annotated with @Message that acts as an incoming "play" message. It returns a reply to the caller as a ThrowMessage containing the player's name and move. Each incoming message will be modeled as a method.