Interface | Description |
---|---|
Actor |
Actors are objects which send messages to each other and which process only one message at a time.
|
ExceptionHandler |
As responses can be received either synchronously or asynchronously,
control flow can be somewhat obscure and the code to catch exceptions
can be error prone.
|
Mailbox |
A mailbox is a lightweight thread with an inbox for incoming messages and an outbox for outgoing messages.
|
MailboxFactory |
Creates Mailboxes and provides access to the thread manager.
|
Class | Description |
---|---|
JAEvent |
Used to send a request to an actor without having to wait for a response.
|
JAFuture |
Used both in the program's main method and in testing to send a request to an actor and wait for a response.
|
JAIterator |
Iterates over its process method and works within any actor which supports 2-way messages.
|
JAMailboxFactory |
Implements MailboxFactory.
|
JANoResponse |
Used with on-way messages (Events) to signal that no response is required.
|
JANull |
Represents a null when exiting JAIterator.iterate.
|
RP<RESPONSE_TYPE> |
Types the response.
|
JActor is blazing fast, passing messages at a rate of up to 1 billion messages per second. It achieves this by using synchronous operations whenever possible. Indeed, JActor is fast enough that you can use actors everywhere.
On the other hand, it is easy enough to force an actor to operate asynchronously--without changing how messages are sent to it. So I/O and computations taking longer than a few microseconds can execute on a different thread. Jactor is entirely free of locks and parallel operations are easy to code, making JActor ideal for applications that need to scale vertically.
Every program needs a MailboxFactory. It allocates the threads used by JActor and creates the mailboxes used by the actors. The MailboxFactory.newMailboxFactory method takes one argument--the number of threads to be allocated. This should be at least as large as the number of hardware threads on your computer, but add additional threads to support parallel I/O operations.
MailboxFactory mailboxFactory = JAMailboxFactory.newMailboxFactory(t);
The MailboxFactory.close method is used to halt the threads as they become idle. A call to this method is not required unless you are running a series of tests, where each test creates a new mailbox factory.
mailboxFactory.close();
Mailboxes are light-weight threads.
Every actor needs a mailbox. It is used for passing messages between actors. But actors can share a common mailbox and when they do, all messages passed between them are handled synchronously. Actors which exchange a lot of messages should, whenever possible, share the same mailbox.
A mailbox is created by calling the MailboxFactory.createMailbox method.
Mailbox mailbox = mailboxFactory.createMailbox();
Mailboxes have a getMailboxFactory method, so if you have one mailbox you can easily create another.
Mailbox mailbox2 = mailbox.getMailboxFactory().createMailbox();
An actor with an asynchronous mailbox operates on a separate thread, which is to say that it does not synchronously process messages from other actors.
The createAsyncMailbox method on MailboxFactory creates an asynchronous mailbox.
Mailbox asyncMailbox = mailboxFactory.createAsyncMailbox();
The isAsync method on mailbox can be used to see if a mailbox is asynchronous.
boolean async = mailbox.isAsync();
Actors are thread-safe objects which exchange request and response messages. Most message passing is 2-way, and an actor expects a response, or an exception, when a request is sent.
There are three types of actors: JLPCActor, JBActor and JCActor. They all implement the Actor interface and take a single argument, a mailbox, in their constructors.
Actors have a getMailbox method, so if you have an actor you can create a mailbox or an asynchronous mailbox.
MailboxFactory mailboxFactory = actor.getMailbox().getMailboxFactory(); Mailbox mailbox = mailboxFactory.createMailbox(); Mailbox asyncMailbox = mailboxFactory.createAsyncMailbox();
To send a request message (an Object) to an actor from non-actor code, you can use either JAFuture or JAEvent. The JAFuture.send method sends the request, waits for and then returns a result. And if there was an exception while processing the request, even if it occurred on another thread, the exception is rethrown. JAFuture is used mostly in testing, or to start up a top-level actor, as it ties up a thread until a response is received.
JAFuture future = new JAFuture(); Object result = future.send(actor, request);
In contrast, JAEvent.send is used to send a request message without waiting for a response. Any response or exception resulting from the processing of the request message is discarded. JAEvent can be used by asynchronous code, e.g. in a CometD application, to pass events to an actor for processing.
JAEvent jaEvent = new JAEvent(); jaEvent.sendEvent(actor, request);
The application logic for processing a request need not distinguish between 1-way and 2-way requests.
When an actor receives a request object, it is also given an accompanying RP object. The processResponse method on the RP class handles the transport and/or processing of the response.
protected void processRequest(Object request, RP rp) throws Exception { rp.processResponse(request); //echo back the request }
RP does have a second method, isEvent, which can be used to detect 1-way messaging. When isEvent returns true, a call to the processResponse method does nothing.
boolean event = rp.isEvent();
The send method of an actor is used to send a request to another actor. And the 3rd parameter is a RP object. But the RP object sent by one actor is often not the same object that is received by the other actor. The JActor code does a lot of intermediation with RP objects.
send(anotherActor, someRequest, new RP() { public void processResponse(Object response) { System.out.println("sent " + someRequest + " and got back " + response); } });
Trapping exceptions using try/catch when there are a lot of callbacks like RP can be tedious and error prone. So instead we use an ExceptionHandler. The ExceptionHandler interface defines one method, process, for processing an exception. Calling the setExceptionHandler method of an actor result in all exceptions being passed to the application's own exception handler. If an actor does not have an exception handler, exceptions which occur while processing a request message are passed back to the actor which sent the request, recursively.
protected void processRequest(Object request, RP rp) throws Exception { setExceptionHandler(new ExceptionHandler(){ public void process(Exception ex) { System.err.println("exception occurred while processing request " + request): pr.process(null); } }); . . . }
Writing a loop when a callback is involved is difficult, especially if the number of iterations is so large that a recursive approach results in stack overflow. One answer is to use JAIterator. The JAIterator class has two methods, iterate and process, where the iterate method calls the process method repeatedly until the process returns a result that is non-null--which is then returned as iterate's result.
final int max = 5; RP printResult = new RP() { public void processResponse(Object rsp) { System.out.println(rsp); } }; (new JAIterator() { int i; int r = 1; public void process(RP rp) throws Exception { if (i >= max) rp.processResponse(new Integer(r)); else { i += 1; r = r * i; rp.processResponse(null); } } }).iterate(printResult);
Sometimes however we want iterate to return a null result. To do this, we just have the processResponse method return a JANull object, which the iterate method recognizes as being non-null but which it converts to a null when returning the result.
The SMBuilder class builds and runs a state machine, and can be very helpful when the application logic gets a bit complex and the resulting mess of code becomes unreadable. State machines are quite commonly used with actors. They let you focus on the application logic and avoid having to deal with low-level control flow.
A good example of this can be found in the Factory class, though it only makes limited use of SMBuilder's capabilities.
SMBuilder smb = new SMBuilder(); smb._send(actor, include); smb._if(actorName == null, "fin"); smb._send(actor, new SetActorName(actorName)); smb._send(getActor(), new RegisterActor(actor)); smb._label("fin"); smb._return(actor); smb.call(rp);
Parallel processing is not at all difficult. You create some asynchronous actors and then do a series of calls to send, aggregating the responses. The aggregated response can then be returned as a result once the last response has been received.
Benchmarks for parallelism are probably the simplest of all, as the responses do not need to be aggregated. The JAParallel class was developed specifically to support this, SharedMailboxTest being a good case in point.
An actor that implements publish/subscribe provides a means of broadcasting events. Subscribers are actors which have been "signed up" to recieve the published requests. Requests then are broadcast by wrapping them in a publish request and sending them to the publish/subscribe actor.
Implemented using 2-way messages, the response of a publish request holds the count of the number of subscribers which have received and processed the request.
There is no particular order in which subscribers receive requests. And the request is sent to all subscribers at once without waiting for a response. Only the response to the publish request is delayed until all subscribers have responded.
Copyright © 2012. All Rights Reserved.