What are Actors?
Actors are independent units of computation that:
- Have private state - no other code can access it directly
- Communicate via messages - the only way to interact
- Process one message at a time - no internal concurrency issues
- Can spawn child actors - creating supervision hierarchies
Your First Actor
Let's create a simple counter actor:
actor Counter {
var count: i64 = 0
// Fire-and-forget message
receive Increment {
count += 1
}
// Message with parameter
receive Add(n: i64) {
count += n
}
// Request-response message
receive GetCount -> i64 {
count
}
}
Spawning Actors
Use the spawn keyword to create actor instances:
fn main() {
// Spawn a new counter actor
let counter = spawn Counter
// Send messages
send(counter, Increment)
send(counter, Add(5))
// Request and wait for response
let current = ask(counter, GetCount)
print("Count: {current}") // Count: 6
}
Send vs Ask
Two ways to communicate with actors:
| Method | Behavior | Use When |
|---|---|---|
send |
Fire-and-forget, returns immediately | You don't need a response |
ask |
Waits for response | You need data back from the actor |
Lifecycle Hooks
Actors can respond to lifecycle events:
actor Worker {
var task_count: i64 = 0
// Called when actor starts
on_start() {
print("Worker starting up...")
}
// Called when actor stops
on_stop() {
print("Worker shutting down. Processed {task_count} tasks.")
}
// Called when recovering from checkpoint
on_resume() {
print("Worker resuming from checkpoint...")
}
receive DoTask(task: Task) {
task_count += 1
// Process task...
}
}
Supervision
Supervisors monitor child actors and handle failures:
supervisor AppSupervisor {
// What to do when a child fails
strategy: OneForOne, // Only restart the failed child
// Restart limits
max_restarts: 5,
within: Duration::seconds(60),
// Child actors to supervise
children: [
child(DatabaseConnection, restart: Always),
child(WebServer, restart: Always),
child(BackgroundWorker, restart: Transient)
]
}
Restart Strategies
OneForOne: Only restart the failed child
OneForAll: Restart all children when one fails
RestForOne: Restart failed child and all started after it
Practical Example: Task Queue
Let's build a simple task queue with a worker pool:
struct Task {
id: String,
data: String
}
actor Worker {
receive Process(task: Task) -> String {
print("Processing task {task.id}")
// Simulate work
Thread::sleep(Duration::ms(100))
"Completed: {task.id}"
}
}
actor TaskQueue {
var workers: List<ActorRef<Worker>>
var next: i64 = 0
fn new(worker_count: i64) -> Self {
let workers = (0..worker_count)
.map(|_| spawn Worker)
.collect()
TaskQueue { workers, next: 0 }
}
receive Submit(task: Task) -> String {
// Round-robin to workers
let worker = workers[next % workers.len()]
next += 1
ask(worker, Process(task))
}
}
fn main() {
let queue = spawn TaskQueue::new(4)
for i in 1..10 {
let task = Task { id: "task-{i}", data: "data" }
let result = ask(queue, Submit(task))
print(result)
}
}
Try It Yourself
Extend the TaskQueue to track completed tasks. Add a GetStats
message that returns the total number of tasks processed by each worker.
Summary
In this tutorial, you learned:
- The actor model and its benefits for concurrency
- Defining actors with
receivehandlers - Spawning actors and sending messages
- The difference between
sendandask - Lifecycle hooks for initialization and cleanup
- Building supervised actor hierarchies
In the next tutorial, we'll integrate AI into our actors by building an AI-enhanced data processing pipeline.