-
Notifications
You must be signed in to change notification settings - Fork 1
Modes of Operation
Synchronous operations are always faster than asynchronous operations, but there are times when asynchronous operation is required. Worse, an asynchronous operation typically requires many other operations to be asynchronous as well. This is why it is a very good thing to write code which can operate both synchronously and asynchronously.
The Actor class, and its subclasses, process messages both synchronously and asynchronously. The timing of these two modes of operation differs most when message processing is brief, with synchronous processing being about 110 times (11,000%) faster than asynchronous processing.
For an actor to operate asynchronously, it requires a mailbox. Incoming messages which can not be processed synchronously are added to the actor's mailbox for processing on a separate thread. The deciding factor for when a message must be processed asynchronously is determined by the mailbox of the actor which sent the request and the mailbox of the actor which received the request. When both actors are using the same mailbox, the receiving actor can safely process the message synchronously. Otherwise the message must be processed asynchronously.
There is one exception to this rule. Immutable actors, which are actors whose state never changes, can safely process all the messages they receive synchronously and do not use a mailbox. We designate an actor as immutable by constructing it without a mailbox. But there are restrictions placed on immutable actors.
- An immutable actor must not change its state.
- The state of an immutable actor must not be changed by any other means. And
- An immutable actor can only send messages to other immutable actors.
The behavior of an actor does differ depending on its mode of operation, so some care needs to be exercised in the patterns we use to ensure that the code is not sensitive to these differences. The difference between synchronous and asynchronous operation is, of course, the time when a message is processed. Time to look at some code.
First we define AMessage, to which we can bind some processing logic.
case class AMessage()
Actor A binds AMessage to afunc, which prints when afunc started and ended, and when it got a result from another actor. The constructor for this actor has two parameters: (1) mb, the mailbox of the actor, and (2) sub, the actor to which a message is to be passed.
class A(mb: Mailbox, sub: Actor) extends Actor(mb, null) {
bind(classOf[AMessage], afunc)
def afunc(msg: AnyRef, rf: Any => Unit) {
println("start afunc")
sub(msg) {rsp =>
println("got result")
rf("all done")
}
println("end afunc")
}
}
Actor B simply returns some result on receiving AMessage. Its constructor has one parameter, mb, the actor's mailbox.
class B(mb: Mailbox) extends Actor(mb, null) {
bind(classOf[AMessage], bfunc)
def bfunc(msg: AnyRef, rf: Any => Unit){rf("ta ta")}
}
Our first test is to pass a message synchronously, i.e. with both actors using the same mailbox.
val mb1 = new Mailbox
val b = new B(mb1)
val mb2 = new Mailbox
In this case, the results are received by actor A immediately.
synchronous test
start afunc
got result
end afunc
Our second test is to pass the message asynchronously, i.e. with each actor using a different mailbox.
println("synchronous test")
Future(new A(mb1, b), AMessage())
println("asynchronous test")
Future(new A(mb2, b), AMessage())
Now actor A does not receive the result until after afunc returns.
asynchronous test
start afunc
end afunc
got result
Because synchronous processing is so much faster than asynchronous processing, actors should, as much as possible, use the same mailbox. So when should they use a different mailboxes? One clear case is when an actor blocks for I/O.
When an actor blocks for I/O, it ties up a thread until the operation is completed. So if you have too many actors blocking for I/O you will be using a lot of threads. And that uses up a lot of memory, fast. So it is best to have only a few actors which perform I/O. For example, you can have one actor which reads the entire contents of a file, the file pathname being passed in a message and the result returned being the file contents. But now you want this file reader actor to have its own mailbox, because when it blocks for I/O it will block the execution of all the other actors using the same mailbox. So in general, every actor which blocks for I/O should have its own mailbox.
A second clear case of when you should be using different mailboxes is when a server needs to process multiple requests in parallel. In this case you want all the actors involved in processing the same request to use the same mailbox, but have a separate mailbox for each request.