The slot table split (ROADMAP_v0.5 phase 2): slot lookup is lock-free, the
run path (yield/park/unpark/pop/resume) takes zero locks beyond the queue
mutex itself, and SharedState shrinks to { run_queue }.
Core pieces:
- src/raw_mutex.rs: hand-rolled 3-state futex mutex (Drepper mutex3),
non-poisoning; the guard enters NoPreempt, which both bounds hold times and
structurally closes the unwind-under-lock hole (the stop sentinel shares
the gate). Used for per-slot cold data, the free list, and the stack pool.
- Fixed slab Box<[Slot]> (default max_actors = 16_384, ~4 MiB, align(128)
against false sharing). Slots never move -> stable addresses, lock-free
index. Exhaustion panics loudly, naming Config::max_actors(n) as the fix.
Unbounded/segmented slab stays deferred (see ROADMAP).
- Per-slot AtomicU64 packing (generation << 32 | state), states
Vacant/Queued/Running/RunningNotified/Parked/Done. Every transition CASes
the packed word, so the generation check is atomic with the transition: no
ABA, no spurious unparks on recycled slots. RunningNotified replaces the
pending_unpark bool (the lost-wakeup window is a state, not a flag) and
uniformly fixes a LATENT LOST WAKEUP in the old Blocking-IO completion
path, which set the result for a still-Running actor without flagging it.
- Invariant: a pid is in the run queue at most once (pushes pair 1:1 with
transitions into Queued; only the scheduler does Queued->Running). This is
what makes phase 3's bounded rings sound.
- sp -> relaxed AtomicUsize; stop flag + first-resume closure as AtomicPtrs
(closure double-boxed for a thin pointer, swap-to-take): the resume path is
fully atomic.
- finalize_actor: Done published under the dying slot's cold lock (join's
check-or-register is linearized by it); link cascade locks peers ONE AT A
TIME (cold locks are leaves) with the acyclicity argument written at the
site; link() registers on the target first, then self (stale self-entries
are benign, every walk re-verifies the peer's word).
- Termination by counters: live_actors incremented in spawn pre-enqueue,
decremented at the very END of finalize after all wakeup enqueues; exit on
io_out == 0 (read before the queue lock, phase-1 ordering) && queue empty
&& live == 0. Soundness note at the site: any enqueue targets a live actor.
- spawn boxes the closure and acquires the stack BEFORE any runtime lock: no
allocation ever happens under a global lock anymore.
- with_runtime/try_with_runtime now enter NoPreempt for their full span.
This fixes a bug the rework exposed: install_actor allocated with
preemption enabled while the RUNTIME RefCell borrow was live; a timeslice
preemption there migrates the actor across OS threads and the borrow guard
increments one thread's RefCell count and decrements another's — underflow
to 'permanently mutably borrowed', cascading panics (caught by stress
suite: deterministic non-unwinding-panic abort in lost_wakeup_many_pairs).
The old code was safe only by accident; now it's structural.
- Behavior note: request_stop on a RUNNING target now marks it
RunningNotified, so its next park returns immediately to an observation
point — faster stop observation; parked/queued/done semantics unchanged.
Validated: 22 suites green in release (1/2/8-thread oversubscribed on the
1-core sandbox) and in debug with all debug_asserts live; stress suite x5.
68 lines
2.3 KiB
Rust
68 lines
2.3 KiB
Rust
//! # smarm — Silly Marks Abstract Rust Machine
|
|
//!
|
|
//! Erlang-style green-thread actor concurrency for Rust.
|
|
//!
|
|
//! Multi-threaded: N scheduler OS threads (default: one per CPU) share a
|
|
//! single global run queue behind a `Mutex`. Actors communicate by sending
|
|
//! `Send` messages over channels; every actor has a supervisor. Synchronisation
|
|
//! primitives — `Mutex<T>` with mandatory lock timeouts, channel `recv`,
|
|
//! `sleep`, and epoll-backed `wait_readable`/`wait_writable` — all park the
|
|
//! green thread, never the OS thread.
|
|
//!
|
|
//! See `LOOM.md` for the design intent and the deferred-for-later list.
|
|
|
|
pub mod stack;
|
|
pub mod context;
|
|
pub mod preempt;
|
|
pub mod pid;
|
|
pub mod actor;
|
|
pub mod channel;
|
|
pub mod scheduler;
|
|
pub mod supervisor;
|
|
pub mod timer;
|
|
pub mod io;
|
|
pub mod mutex;
|
|
pub mod monitor;
|
|
pub mod link;
|
|
pub mod gen_server;
|
|
pub mod runtime;
|
|
pub(crate) mod raw_mutex;
|
|
pub mod trace;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Global allocator
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[global_allocator]
|
|
static ALLOCATOR: preempt::PreemptingAllocator = preempt::PreemptingAllocator;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public API re-exports
|
|
// ---------------------------------------------------------------------------
|
|
|
|
pub use channel::{channel, Receiver, RecvError, Sender};
|
|
pub use gen_server::{CallError, CastError, GenServer, ServerRef};
|
|
pub use link::{link, trap_exit, unlink, ExitSignal};
|
|
pub use monitor::{demonitor, monitor, Down, DownReason, Monitor, MonitorId};
|
|
pub use mutex::{LockTimeout, Mutex, MutexGuard};
|
|
pub use pid::Pid;
|
|
pub use runtime::{init, Config, Runtime};
|
|
pub use scheduler::{
|
|
block_on_io, request_stop, run, self_pid, sleep, spawn, spawn_under, wait_readable,
|
|
wait_writable, yield_now, JoinError, JoinHandle,
|
|
};
|
|
pub use supervisor::{ChildSpec, OneForOne, Restart, Signal, Strategy};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// check!()
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Voluntarily check whether this actor's timeslice has expired, yielding
|
|
/// if so.
|
|
#[macro_export]
|
|
macro_rules! check {
|
|
() => {
|
|
$crate::preempt::maybe_preempt()
|
|
};
|
|
}
|