actors
v0.0.17The multicore actor runtime that powers Riot
riot add actors actors = "0.0.17" - Last release
- Apr 10, 2026
- License
- Apache-2.0
- Dependencies
- 1
- Dependents
- 0
- Installs
- 51
- Published versions
- 9
- Owner
-
@leostera
Examples (1)
README Expand Collapse
Actors
Multicore actors for Riot.
actors is Riot's actor runtime package that everything runs on: std,
blink, suri, and the rest of the ecosystem ultimately run on top of these
process, mailbox, timer, and scheduler semantics.
This package exists so that runtime-level libraries and lower-level Riot code can depend on that layer directly and explicitly. It gives you lightweight processes, typed message passing, links and monitors, timers, async syscalls, and a multicore runtime that can spread runnable actors across scheduler workers with work stealing.
Install
riot add actors
What value it gives you
Everything built on Riot gets these properties because it runs on actors:
- Erlang-style processes and mailboxes without pulling in the whole
stdsurface; - parallel execution across multiple worker schedulers instead of a single-core event loop;
- failure-aware process relationships through links and monitors;
- timer-based wakeups and async syscall integration inside the same actor runtime;
- a small runtime package you can build other Riot infrastructure on top of.
You add actors as a direct dependency when you are working at the runtime
layer itself. In practice that usually means:
- build systems and task executors;
- schedulers, brokers, queues, and orchestration layers;
- protocol runtimes and infrastructure packages;
- systems code that wants actor semantics without the rest of the application stack.
If you are building a normal application, service, or CLI, you are still using
actors, just indirectly through std. Most application code should start
with std, because it already builds on the same runtime and gives you the
rest of the practical surface area around it.
Quick start
open Std
open Actors
type Message.t +=
| Ping of Pid.t
| Pong
let worker = fun () ->
let sender =
receive ~selector:(function
| Ping sender -> `select sender
| _ -> `skip)
()
in
send sender Pong;
Ok ()
let main = fun ~args:_ ->
let pid = spawn worker in
send pid (Ping (self ()));
receive
~selector:(function
| Pong -> `select ()
| _ -> `skip)
();
Ok ()
let () = run ~main ~args:Env.args ()
The package also ships a runnable example:
riot run -p actors ping_pong
The APIs you will actually use
The most important entry points are:
runto start the runtime;spawnandspawn_linkto create processes;send,receive, andreceive_anyfor mailbox-driven workflows;selfto get the current PID;yieldto spend cooperative reductions explicitly when long-running work should give other actors a chance to run;Process.monitor,Process.link,Process.unlink, andProcess.demonitorfor process lifecycle coordination;Timer.send_afterandTimer.send_intervalfor delayed or repeating messages;syscallfor async source integration through the runtime's reactor.
Most day-to-day actor code stays very small:
- define message constructors on
Message.t spawna process loopsendmessages to itreceivethe messages you care about
How it works
At a high level, actors runs one normal scheduler worker per configured slot
plus a dedicated reactor domain for timers and async I/O.
The important pieces are:
- lightweight processes: spawned actors are green-thread processes with their own mailbox and continuation state rather than OS threads;
- effects for suspension:
receive,yield, and actor-aware syscalls suspend and resume work through OCaml 5 effects, which keeps actor code looking direct instead of callback-driven; - worker-local runnable queues: each scheduler owns a local runnable queue for the actors it is currently driving;
- work stealing: when a worker runs out of local work, it can steal runnable actors from busier workers so execution spreads across cores;
- reactor-driven wakeups: actors waiting on timers or async sources are re-enqueued when those operations become ready.
So the mental model is:
spawncreates an actor and places it on a worker queue- the actor runs until it finishes, yields, blocks on
receive, or waits on async I/O - blocked actors sleep without consuming a worker
- runnable actors can move between workers when the runtime steals work
Runtime model
You do not need to manually assign actors to cores.
The runtime starts one normal worker scheduler per configured slot plus a dedicated reactor domain for timers and async I/O. Runnable actors are placed onto worker-local queues, and idle workers can steal runnable actors from busy workers. That means a burst of independent actors can spread across multiple cores without you having to shard work yourself.
The important semantic guarantees remain actor-centric:
- PIDs are runtime-wide.
sendtargets a PID, not a scheduler.- messages sent from one sender to one recipient are observed in send order.
- blocked actors resume when their mailbox, timer, or async source makes them runnable again.
The runtime is still cooperative at the process level. A process doing a large
amount of CPU work should either block in meaningful runtime operations or call
yield periodically so it shares execution fairly with other runnable actors.
Configuration and observability
The runtime defaults to Riot's normal scheduler sizing, but you can pass an
explicit Actors.Config.t to run when you want to size the worker pool or
change timer resolution.
For debugging and tuning:
enable_trace/disable_traceturn tracing on and off;trace_countersexposes scheduler counters such as steals, failed steals, remote wakeups, and duplicate enqueue races;reset_trace_countersclears those counters between runs.
This is most useful when you are working on infrastructure code and want to understand whether the runtime is balancing work the way you expect.
Example patterns
Patterns that fit naturally in actors:
- coordinator/worker execution pools;
- request/response processes;
- background resource managers;
- mailbox-driven protocol implementations;
- timer-triggered retries and timeouts;
- supervision-like lifecycle coordination built from links and monitors.
Related packages
kernelprovides the lower-level async and runtime primitives thatactorsbuilds on.stdis the higher-level application stack built on the same actor runtime.blinkandsuriare examples of packages that build richer abstractions on top of Riot's concurrency model.
Actors
Multicore actors for Riot.
actors is Riot's actor runtime package that everything runs on: std,
blink, suri, and the rest of the ecosystem ultimately run on top of these
process, mailbox, timer, and scheduler semantics.
This package exists so that runtime-level libraries and lower-level Riot code can depend on that layer directly and explicitly. It gives you lightweight processes, typed message passing, links and monitors, timers, async syscalls, and a multicore runtime that can spread runnable actors across scheduler workers with work stealing.
Install
riot add actors
What value it gives you
Everything built on Riot gets these properties because it runs on actors:
- Erlang-style processes and mailboxes without pulling in the whole
stdsurface; - parallel execution across multiple worker schedulers instead of a single-core event loop;
- failure-aware process relationships through links and monitors;
- timer-based wakeups and async syscall integration inside the same actor runtime;
- a small runtime package you can build other Riot infrastructure on top of.
You add actors as a direct dependency when you are working at the runtime
layer itself. In practice that usually means:
- build systems and task executors;
- schedulers, brokers, queues, and orchestration layers;
- protocol runtimes and infrastructure packages;
- systems code that wants actor semantics without the rest of the application stack.
If you are building a normal application, service, or CLI, you are still using
actors, just indirectly through std. Most application code should start
with std, because it already builds on the same runtime and gives you the
rest of the practical surface area around it.
Quick start
open Std
open Actors
type Message.t +=
| Ping of Pid.t
| Pong
let worker = fun () ->
let sender =
receive ~selector:(function
| Ping sender -> `select sender
| _ -> `skip)
()
in
send sender Pong;
Ok ()
let main = fun ~args:_ ->
let pid = spawn worker in
send pid (Ping (self ()));
receive
~selector:(function
| Pong -> `select ()
| _ -> `skip)
();
Ok ()
let () = run ~main ~args:Env.args ()
The package also ships a runnable example:
riot run -p actors ping_pong
The APIs you will actually use
The most important entry points are:
runto start the runtime;spawnandspawn_linkto create processes;send,receive, andreceive_anyfor mailbox-driven workflows;selfto get the current PID;yieldto spend cooperative reductions explicitly when long-running work should give other actors a chance to run;Process.monitor,Process.link,Process.unlink, andProcess.demonitorfor process lifecycle coordination;Timer.send_afterandTimer.send_intervalfor delayed or repeating messages;syscallfor async source integration through the runtime's reactor.
Most day-to-day actor code stays very small:
- define message constructors on
Message.t spawna process loopsendmessages to itreceivethe messages you care about
How it works
At a high level, actors runs one normal scheduler worker per configured slot
plus a dedicated reactor domain for timers and async I/O.
The important pieces are:
- lightweight processes: spawned actors are green-thread processes with their own mailbox and continuation state rather than OS threads;
- effects for suspension:
receive,yield, and actor-aware syscalls suspend and resume work through OCaml 5 effects, which keeps actor code looking direct instead of callback-driven; - worker-local runnable queues: each scheduler owns a local runnable queue for the actors it is currently driving;
- work stealing: when a worker runs out of local work, it can steal runnable actors from busier workers so execution spreads across cores;
- reactor-driven wakeups: actors waiting on timers or async sources are re-enqueued when those operations become ready.
So the mental model is:
spawncreates an actor and places it on a worker queue- the actor runs until it finishes, yields, blocks on
receive, or waits on async I/O - blocked actors sleep without consuming a worker
- runnable actors can move between workers when the runtime steals work
Runtime model
You do not need to manually assign actors to cores.
The runtime starts one normal worker scheduler per configured slot plus a dedicated reactor domain for timers and async I/O. Runnable actors are placed onto worker-local queues, and idle workers can steal runnable actors from busy workers. That means a burst of independent actors can spread across multiple cores without you having to shard work yourself.
The important semantic guarantees remain actor-centric:
- PIDs are runtime-wide.
sendtargets a PID, not a scheduler.- messages sent from one sender to one recipient are observed in send order.
- blocked actors resume when their mailbox, timer, or async source makes them runnable again.
The runtime is still cooperative at the process level. A process doing a large
amount of CPU work should either block in meaningful runtime operations or call
yield periodically so it shares execution fairly with other runnable actors.
Configuration and observability
The runtime defaults to Riot's normal scheduler sizing, but you can pass an
explicit Actors.Config.t to run when you want to size the worker pool or
change timer resolution.
For debugging and tuning:
enable_trace/disable_traceturn tracing on and off;trace_countersexposes scheduler counters such as steals, failed steals, remote wakeups, and duplicate enqueue races;reset_trace_countersclears those counters between runs.
This is most useful when you are working on infrastructure code and want to understand whether the runtime is balancing work the way you expect.
Example patterns
Patterns that fit naturally in actors:
- coordinator/worker execution pools;
- request/response processes;
- background resource managers;
- mailbox-driven protocol implementations;
- timer-triggered retries and timeouts;
- supervision-like lifecycle coordination built from links and monitors.
Related packages
kernelprovides the lower-level async and runtime primitives thatactorsbuilds on.stdis the higher-level application stack built on the same actor runtime.blinkandsuriare examples of packages that build richer abstractions on top of Riot's concurrency model.