It's probably best to think of the actor model as the ancestor of object oriented programming; and compare it to OOP. However, because (I hope) you're already familiar with OOP I'm going to do the reverse.
For OOP, imagine you have a simple object with code vaguely like this:
integer currentValue = 0;
void incrementMethod(void) {
currentValue = currentValue + 1;
}
void decrementMethod(void) {
currentValue = currentValue - 1;
}
integer getValueMethod(void) {
return currentValue;
}
void setValueMethod(integer value) {
currentValue = value;
}
This has problems with concurrency, because multiple threads/CPUs could execute the methods at the same time and mess each other up. The "actor model" solution is to prevent direct access to the methods and provide isolation between "things" (objects/actors). The consequence is that you need some form of communication between the isolated things. That communication is message passing.
The silly example above could become:
integer currentValue = 0;
void main(void) {
while(running) {
message = getMessage();
switch(messsage->type) {
case INCREMENT_REQUEST:
incrementMethod();
break;
case DECREMENT_REQUEST:
decrementMethod();
break;
case GET_VALUE_REQUEST:
getValueMethod(message->senderID);
break;
case SET_VALUE_REQUEST:
setValueMethod();
break;
default:
sendMessage(senderID, UNKNOWN_REQUEST, NULL);
}
}
}
void incrementMethod(void) {
currentValue = currentValue + 1;
}
void decrementMethod(void) {
currentValue = currentValue - 1;
}
void getValueMethod(messagePort senderID) {
sendMessage(senderID, GETVALUE_REPLY, currentValue);
}
void setValueMethod(integer value) {
currentValue = value;
}
Because the messaging serialises requests and because no data is shared between threads, no other form of concurrency control is necessary. Because no data is shared between threads, it's also highly distributable (e.g. you can have a system where different actors are on different physical computers). Because of the isolation between actors it's "more possible" (at least in theory) to support things like live update (e.g. by replacing an actor's code while the rest of the system is running) and fault tolerance (e.g. triple redundancy with 3 actors on 3 different computers instead of one). Finally, because an actor is able to accept an unknown request it's more extensible (e.g. you could send an ADD_VALUE_REQUEST
message to an actor that it doesn't understand, and then make the actor understand that message type afterwards).
The main problem with (the naive implementation of) the actor model is performance - with one thread per actor plus the overhead of message passing the performance (as compared to OOP) would be extremely bad. There are multiple ways of fixing that, but the most common ways are to use many actors per thread and to allow actors within the same thread to call each other's methods directly (and avoid message passing in that case). With this in mind, you could define the origins of OOP as "actor model with only one thread where all actors can call each other's methods directly" (realising that multi-threading got retrofitted back on top of OOP later).
The other thing I need to point out is that for my example I'm being intentionally "low level" to help you understand what is happening behind the scenes. For a language designed for the actor model, the message handling loop shown above (main()
in its entirety) would be implicit and not explicit; to save the programmer from typing so much stuff, and also so that the compiler can do the optimisations I mentioned.