Previous slide Next slide Toggle fullscreen Open presenter view
History
Carl Hewit et al. 1973: Actors invented for research on artificial intelligence
Gul Agha, 1986: Actor languages and communication patterns
Ericsson, 1995: first commercial use in Erlang/OTP for telecommunications platform
Philipp Haler, 2006: implementation in Scala standard library
Jonas BonΓ©r, 2009: creation of Akka
JosΓ© Valim, 2012: Creation of Elixir
Traditional Synchronization
Multiple threads stepping on each others' toes:
demarcate regions of code with "don't disturb" semantics
make sure that all access to shared state is protected
Synchronization
Multiple threads stepping on each others' toes:
demarcate regions of code with "don't disturb" semantics
make sure that all access to shared state is protected
Primary tools: lock, mutex, semaphore
In Scala every object has a lock: synchronized {...}
class BankAccount {
private var balance = 0
def deposit (amount: Int ): Unit = synchronized {
if (amount > 0 ) balance = balance + amount
}
def withdraw (amount: Int ): Int = synchronized {
if (0 < amount & amount <= balance) {
balance = balance - amount
balance
} else throw new Error ("insufficient funds" )
}
}
What is an Actor?
The Actor Model represents objects and their interactions, resembling human organizations and built upon the laws of
physics.
An Actor:
is an object with identity
has a behavior
only interacts using asynchronous message passing
The Actor Trait
type Receive = PartialFunction [Any , Unit ]
trait Actor {
def receive : Receive
}
A Simple Actor
class Counter extends Actor {
var count = 0
def receive = {
case "incr" => count += 1
}
}
Exposing the State
class Counter extends Actor {
var count = 0
def receive = {
case "incr" => count += 1
case ("get" , customer: ActorRef ) =>
customer ! count
}
}
Actors can send messages to addresses (ActorRef) they know.
Example using the class above
class Printer extends Actor {
def receive = {
case count: Int => println("Printer received count: " + $count)
}
}
Usage:
counter ! "incr"
counter ! "incr"
counter ! "incr"
counter ! ("get" , printer)
How Messages are Sent
trait Actor {
implicit val self: ActorRef
def sender : ActorRef
}
What is an ActorRef
?
It representing an addressable actor reference
Think of ActorRef as the actor's "mailbox address"
abstract class ActorRef {
def ! (msg: Any )(implicit sender: ActorRef ): Unit
def tell (msg: Any , sender: ActorRef ): Unit = this .!(msg)(sender)
}
Sending a message from one actor to the other picks up the sender's address implicitly.
Using the Sender
For example. If I want to send a message back to the actor that invokes 'get'
class Counter extends Actor {
var count = 0
def receive = {
case "incr" => count += 1
case "get" => sender ! count
}
}
Another example
Expand the Actor
Printer
class Printer extends Actor {
def receive = {
case count: Int =>
println(s"[${self.path.name} ] received count: $count " )
sender ! s"Acknowledged count $count from ${self.path.name} "
}
}
Interacting with Printer
class CounterClient (printer: ActorRef ) extends Actor {
def receive = {
case ack: String =>
println(s"[${self.path.name} ] got reply: $ack " )
}
override def preStart (): Unit = {
printer ! 42
printer.tell(99 , self)
}
}
The Actor's Context
In the Actor model, the ActorContext represents the execution context or environment in which an actor runs.
It provides services and tools that the actor can use to:
Create other actors
Change its own behavior dynamically
Access self and sender references
Stop itself or other actors
The Actor type describes the behavior, the execution is done by its ActorContext.
In code
trait ActorContext {
def become (behavior: Receive , discardOld: Boolean = true ): Unit
def unbecome (): Unit
}
Every Actor has an implicit reference to its ActorContext
Example
class ToggleActor extends Actor {
def on : Receive = {
case "switch" =>
println("Turning off..." )
context.become(off)
}
def off : Receive = {
case "switch" =>
println("Turning on..." )
context.become(on)
}
def receive = on
}
Another example
Define a class 'Counter' that extends the 'Actor' trait.
Functional style (no mutable variables)
class Counter extends Actor {
def counter (n: Int ): Receive = {
case "incr" => context.become(counter(n + 1 ))
case "get" => sender ! n
}
def receive = counter(0 )
}
Creating and Stopping Actors
Define the ActorContext trait, which provides the execution environment for an Actor.
This trait exposes methods that an actor can use to interact with the actor system:
to create new actors
to stop existing actors (including itself)
trait ActorContext {
def actorOf (p: Props , name: String ): ActorRef
def stop (a: ActorRef ): Unit
}
An Actor Application
Define the main actor that drives the application logic
class CounterMain extends Actor {
val counter: ActorRef = context.actorOf(Props [Counter ], "counter" )
counter ! "incr"
counter ! "incr"
counter ! "incr"
counter ! "get"
def receive : Receive = {
case count: Int =>
println(s"Count was $count " )
context.stop(self)
}
}
The Main App
Define the main application entry point
object CounterMainApp extends App {
val system = ActorSystem ("CounterSystem" )
system.actorOf(Props [CounterMain ], "main" )
}
The Actor Model of Computation
Upon reception of a message, an actor can do any combination of the following:
Send messages β communicate with other actors asynchronously.
Create actors β spawn child actors to delegate work or structure the system hierarchically.
Designate the behavior for the next message β dynamically change its own behavior for future messages.
Actors encapsulate both state and behavior, allowing safe, lock-free concurrency by reacting to messages.
Actor Encapsulation
Actors are isolated: external code cannot access their internal state or behavior directly .
Only interaction: asynchronous message passing via known addresses (ActorRef
):
every actor knows its own address (self
)
creating an actor returns its address
addresses can be shared and passed in messages (e.g., via sender
)
This model enforces isolation and prevents shared-memory issues like race conditions.
Actor Encapsulation
Actors are fully independent units of execution .
Run locally and concurrently
No shared memory, no global synchronization
Communication is one-way and asynchronous
Like people sending emails: each works independently and responds when ready.
Actor-Internal Evaluation Order
Each actor is effectively single-threaded :
Messages are handled sequentially
A call to context.become
changes behavior before the next message
Processing one message is atomic β no interleaving with other actors
This simplifies reasoning: no need for locks inside an actor.
Actor-Internal Evaluation Order
Actors process one message at a time:
No overlap between message handlers
Behavior changes apply to the next message
Atomicity ensures safe local state updates
This is like synchronized
, but without blocking β instead, messages queue up in the mailbox.
Actor-Internal Evaluation Order
An actor is effectively single-threaded:
messages are received sequentially
behavior change is effective before processing the next message
processing one message is the atomic unit of execution
This has the benefits of synchronized methods, but blocking is replaced by enqueueing a message.
The Bank Account (revisited)
Good practice: define Actor's messages in companion object.
In this case 4 case
classes one for each actor message
object BankAccount {
case class Deposit (amount: BigInt )
case class Withdraw (amount: BigInt )
case object Done
case object Failed
}
The Bank Account (revisited)
class BankAccount extends Actor {
var balance: BigInt = BigInt (0 )
def receive : Receive = {
case Deposit (amount) =>
balance += amount
sender ! Done
case Withdraw (amount) if amount <= balance =>
balance -= amount
sender ! Done
case _ => sender ! Failed
}
}
Actor Collaboration
picture actors as persons
model activities as actors
Transferring Money (0)
object WireTransfer {
case class Transfer (from: ActorRef , to: ActorRef , amount: BigInt )
case object Done
case object Failed
}
Transferring Money (1)
class WireTransfer extends Actor {
def receive : Receive = {
case Transfer (from, to, amount) =>
from ! BankAccount .Withdraw (amount)
context.become(awaitWithdraw(to, amount, sender))
}
def awaitWithdraw (to: ActorRef , amount: BigInt , client: ActorRef ): Receive = ???
}
Transferring Money (2)
class WireTransfer extends Actor {
def awaitWithdraw (to: ActorRef , amount: BigInt , client: ActorRef ): Receive = {
case BankAccount .Done =>
to ! Deposit (amount)
context.become(awaitDeposit(client))
case BankAccount .Failed =>
client ! Failed
context.stop(self)
}
def awaitDeposit (client: ActorRef ): Receive = ???
}
Transferring Money (3)
class WireTransfer extends Actor {
def awaitDeposit (client: ActorRef ): Receive = {
case BankAccount .Done =>
client ! Done
context.stop(self)
}
}
Message Delivery Guarantees
all communication is inherently unreliable
delivery of a message requires eventual availability of channel & recipient
Message Delivery Guarantees
all communication is inherently unreliable
delivery of a message requires eventual availability of channel & recipient
Delivery guarantees:
at-most-once: sending once delivers [0, 1] times
at-least-once: resending until acknowledged delivers (1, ) times
exactly-once: processing only first reception delivers 1 time
Reliable Messaging
Messages support reliability:
all messages can be persisted
can include unique correlation IDs
delivery can be retries until successful
Reliable Messaging
Messages support reliability:
all messages can be persisted
can include unique correlation IDs
delivery can be retries until successful
Reliability can only be ensured by business-level acknowledgement.
Making the Transfer Reliable
log activities of WireTransfer to persistent storage
each transfer has a unique ID
add ID to Withdraw and Deposit
store IDs of completed actions within BankAccount
Message Ordering
If an actor sends multiple messages to the same destination, they will not arrive out of order (this is Akka-specific).
Recap
Actors are fully encapsulated, independent agents of computation.
Messages are the only way to interact with actors.
Explicit messaging allows explicit treatment of reliability.
The order in which messages are processed is mostly undefined.
Starting Out with the Design
Imagine giving the task to a group of people, dividing it up.
Consider the group to be of huge size.
Start with how people with different tasks will talk with each other.
Consider these βpeopleβ to be easily replaceable.
Draw a diagram with how the task will be split up, including communication lines.
Let It Crash: The Philosophy
Embrace failure rather than prevent it.
Errors are expected in distributed systems.
Defensive programming leads to complexity and rigidity.
The Actor model isolates failures : actors crash and restart without affecting others.
In Erlang/Elixir: "fail fast, recover quickly"
Why Let It Crash Works
Each actor is isolated: a crash affects only that actor .
When an actor fails, its supervisor can restart or handle it.
No need for complex error handling inside each actor.
Simpler, more resilient systems emerge from letting small parts fail.
Supervision Trees
Actors can supervise child actors:
Supervisors detect failures and apply restart strategies .
Failures don't propagate chaos; they're contained .
The structure forms a supervision hierarchy (tree).
val child = context.actorOf(Props [Worker ], "worker" )
Trees reflect modularity and control scope of recovery.
Supervision Strategies
Common strategies include:
Restart
: recreate the actor fresh.
Resume
: ignore failure and continue.
Stop
: terminate the actor.
Escalate
: let the failure bubble up.
override val supervisorStrategy =
OneForOneStrategy () {
case _: ArithmeticException => Resume
case _: NullPointerException => Restart
case _: Exception => Stop
}
Designing for Resilience
Design tips:
Compose the system from small, crashable actors .
Assign supervision clearly: who is responsible for whom?
Avoid complex local try/catch blocks β rely on supervision.
Structure follows failure boundaries.
Resilience is an architectural choice, not an afterthought.
A selection of events in the history of Actors
First published by Hewitt, Bishop, and Steiger in 1973 to create a model in which they can formulate the programs for their artificial intelligence research.
One of Hewitt's students published his Ph.D thesis in 1986. Gul Agha formulated Actor languages, so how to write down Actor programs, how to reason about them. He described communication patterns between Actors, and how to use them to solve real problems.
In the same year, Ericsson started developing a programming language named Erlang. This is a purely functional programming language, whose concurrency model is based on Actors. And which was then subsequently used in commercial products.
In 1995, Ericsson presented their new telecommunications platform. Which was highly successful. It quoted a reliability of 9 9ths, which means that there was only about 30 miliseconds of downtime per year.
This robustness and resilience was made possible by the Actor model.
Inspired by the success of Ericsson's Erlang, Philipp Haller added Actors to the Scala standard library in 2006.
Jonas BonΓ©r was then influenced by Erlang, as well as the Scala Actors to create Akka in 2009. Akka is an active framework on the JVM with Java and Scala APIs making the Actor model available to a wide range of developers.
Actors are applicable in a wide range of problems, but let's first see a few problems which motivate our use of Actors.
Make sure that when one thread is working with the data, the others keep out. Like you put a don't disturb sign on your hotel door.
So let's say in this example we're looking at the balance, as the data to be protected.
And what we need to do is to put a fence around it, so that when one thread is working with the data, say thread 1.
That this one has exclusive access to it. Which means threat 2 here, if it tries to access the data, it will actually be denied access at this time.
And it has to wait until threat 1 is finished with it. This way, the balance will be protected. And all modifications done on it are done in a consistent fashion, one after the other. We also say serialized.
The primary tools for achieving this kind of synchronization are lock or mutex. Which is basically the same concept as shown just previously. Or a semaphore where the difference is that multiple but only a defined number of threads can enter this region.
In Scala, every object has an associated lock. Which you can access by calling the synchronized method on it. And it accepts a code lock which will be executed in this protected region.
How do we apply this to the bank account to make it synchronized?
The deposit method also modifies the balance. And if it was not synchronized, then it could modify it without protection. And once the withdrawal writes the balance back here, it would override the override the update performed by deposit at the same time.
This is to illustrate that all accesses to balance need to be synchronized, and not just the one which we have proven to be problematic.