🎭 What is an Actor?
In traditional multi-threaded programming, threads communicate by mutating shared memory under mutex locks. This design leads directly to race conditions, deadlocks, and unpredictable timing bugs.
Vela adopts the Actor Model: an actor is an autonomous execution unit that combines:
- Private Encapsulated State: An actor's fields belong exclusively to that actor. External code cannot read or modify them directly.
- A Mailbox Channel: Incoming messages queue in a lockless channel associated with the actor.
-
A Sequential Message Loop: The actor processes
exactly one message at a time in its own thread, executing the
matching
onhandler.
📝 Syntax and Declaration
An actor is declared with the actor keyword, followed by
its state parameters and message handlers:
actor Counter(count: int)
on Increment(step: int)
count += step
fi
on Get(reply: Chan[int])
reply.dispatch(count)
fi
fi
Spawning and Dispatching Messages
Actors are spawned with the spawn expression, returning
an Actor[T] handle:
task main()
fixed c = spawn Counter(0)
; Dispatch messages asynchronously (both method and functional syntax supported)
c.dispatch(Counter.Increment(5))
dispatch(c, Counter.Increment(10))
; Request-reply pattern via channels
fixed reply = chan_new<int>(1)
c.dispatch(Counter.Get(reply))
fixed result = reply.wait_and_receive()
show("Counter is: ", result) ; 15
; Graceful shutdown
c.close()
fi
⚖️ Actors vs. Events: What is the Difference?
Because both Actors and Events respond to occurrences, developers often wonder how they differ and when to use each:
| Dimension | Actors (actor) |
Events (event) |
|---|---|---|
| Execution Context | Runs on its own concurrent thread/fiber loop. | Runs synchronously within the caller's thread. |
| State Ownership | Owns encapsulated private state (share nothing). | Observes external state (fields, tasks, lifecycle). |
| Invocation Trigger | Dequeuing a message from its mailbox channel. |
Field mutation (on .hp), task entry/exit, or
lifecycle.
|
| Concurrency | Asynchronous, decoupled, non-blocking message send. |
Synchronous immediate hook with
alters(...) verification.
|
| Primary Purpose | Safe concurrent state management and worker pipelines. | Reactive UI triggers, invariant validation, and auditing. |
The Powerful Synergy: Bridging Events to Actors
Actors and events work best together. An event hook can detect a critical state transition and immediately dispatch an alert message into an Actor's mailbox:
shape Thermometer
temperature: int
monitor: Actor[Monitor]
event on_temp on .temperature auto
when .temperature > 100
.monitor.dispatch(Monitor.OverheatAlert(.temperature))
fi
fi
fi
The reactive trigger is immediate and verified at compile time, while the heavy processing is handled asynchronously by the actor without blocking the caller!
⚙️ Under the Hood: ISO C11 Codegen & M:N Scheduler
Vela compiles actors directly to efficient, standard ISO C11 multiplexed on an M:N green thread scheduler pool:
-
Context Struct: A heap-allocated context struct
(
vela_actor__Name__ctx) stores the mailbox handle and mutable actor fields across step executions. -
Step Function: A
static void Name__step(void* raw_ctx)function processes batches of up to 16 messages sequentially withvela__rt__chan_try_recv. -
Message Tag Dispatch: The step switches on
msg.tag, extracts payload parameters from the tagged union, and executes the handler body. -
Auto-Parking & Drain: If more messages remain
in the queue,
Name__stepreschedules itself on the background pool. If the mailbox is empty, the actor parks with zero CPU overhead until a new message is dispatched. When closed and drained, the context is freed automatically.
🛡️ State Safety Guarantees
-
Zero Data Races on Actor State: External code
cannot read
c.count; any attempt triggers a compilation error:actor state is private. - No Mutex Lock Contention: Handlers execute sequentially within the actor's thread, meaning handlers can mutate state freely without locking.
-
Linear Ownership Transfer: Sending messages
carrying
own Trelinquishes sender ownership, guaranteeing single-thread ownership across concurrency boundaries.