An actor is similar to a class, but with one critical difference: an actor can have behaviours.
A behaviour is like a function, except that functions are synchronous and behaviours are asynchronous. In other words, when you call a function, the body of the function is executed immediately, and the result of the call is the result of the body of the function. This is just like method invocation in any other object-oriented language.
But when you call a behaviour, the body is not executed immediately. Instead, the body of the behaviour will execute at some indeterminate time in the future.
A behaviour looks like a function, but instead of being introduced with the keyword
fun, it is introduced with the keyword
Like a function, a behaviour can have parameters. Unlike a function, it doesn’t have a receiver capability (a behaviour can be called on a receiver of any capability) and you can’t specify a return type.
So what does a behaviour return? Behaviours always return
None, like a function without explicit result type, because they can’t return something they calculate (since they haven’t run yet).
let name: String
var _hunger_level: U64 = 0
new create(name': String) =>
name = name'
be eat(amount: U64) =>
_hunger_level = _hunger_level - amount.min(_hunger_level)
Here we have an
Aardvark that can eat asynchronously. Clever Aardvark.
If you are familiar with actor-based languages like Erlang, you are familiar with the concept of “message passing”. It’s how actors communicate with one another. Behaviours are the Pony equivalent. When you call a behavior on an actor, you are sending it a message.
If you aren’t familiar with message passing, don’t worry about it. We’ve got you covered. Keep reading and all will be explained.
Since behaviours are asynchronous, it’s ok to run the body of a bunch of behaviours at the same time. This is exactly what Pony does. The Pony runtime has its own cooperative scheduler, which by default has a number of threads equal to the number of CPU cores on your machine. Each scheduler thread can be executing an actor behaviour at any given time, so Pony programs are naturally concurrent.
Actors themselves, however, are sequential. That is, each actor will only execute one behaviour at a time. This means all the code in an actor can be written without caring about concurrency: no need for locks or semaphores or anything like that.
When you’re writing Pony code, it’s nice to think of actors not as a unit of parallelism, but as a unit of sequentiality. That is, an actor should do only what has to be done sequentially. Anything else can be broken out into another actor, making it automatically parallel.
In the example below, the
Main actor calls a behaviour
call_me_later which, as we know, is asynchronous, so we won’t wait for it to run before continuing. Then, we run the method
env.out.print, which is also asynchronous, and will print the provided text to the terminal. Now that we’ve finished executing code inside the
Main actor, the behaviour we’ve called earlier will eventually run, and it will print the last text.
new create(env: Env) =>
env.out.print("This is printed first")
be call_me_later(env: Env) =>
env.out.print("This is printed last")
Since all this code runs inside the same actor, and the calls to the other behaviour
env.out.print are sequential as well, we are guaranteed that
"This is printed first" is always printed before
"This is printed last".
Why is this safe?
Because of Pony’s capabilities secure type system. We’ve mentioned reference capabilities briefly before when talking about function receiver reference capabilities. The short version is that they are annotations on a type that make all this parallelism safe without any runtime overhead.
We will cover reference capabilities in depth later.
Actors are cheap
If you’ve done concurrent programming before, you’ll know that threads can be expensive. Context switches can cause problems, each thread needs a stack (which can be a lot of memory), and you need lots of locks and other mechanisms to write thread-safe code.
But actors are cheap. Really cheap. The extra cost of an actor, as opposed to an object, is about 256 bytes of memory. Bytes, not kilobytes! And there are no locks and no context switches. An actor that isn’t executing consumes no resources other than the few extra bytes of memory.
It’s pretty normal to write a Pony program that uses hundreds of thousands of actors.
Like classes, actors can have finalisers. The finaliser definition is the same (
fun _final()). All guarantees and restrictions for a class finaliser are also valid for an actor finaliser. In addition, an actor will not receive any further message after its finaliser is called.