Actor Over Mutex
For terminal state management, the Actor pattern eliminates the race conditions that Mutex can only mitigate.The Problem: Concurrent State Access
Copy
╔═════════════════════════════════════════════════════════════════╗
║ THE TERMINAL CONCURRENCY PROBLEM ║
╠═════════════════════════════════════════════════════════════════╣
║ ║
║ Multiple Sources Want Terminal State Simultaneously: ║
║ ║
║ PTY Output Thread ──────┬ ║
║ │ ║
║ User Input Handler ─────┼────────▶ Terminal State ║
║ │ (Grid) ║
║ Renderer Timer ─────────┘ ▲ ║
║ │ ║
║ Close Handler ║
║ │ ║
║ Resize Handler ║
║ ║
║ ─────────────────────────────────────────────────────────── ║
║ ║
║ Without Protection: DATA RACE ║
║ ║
║ Thread A: Reading cell[5,10] for render ║
║ Thread B: Writing cell[5,10] from PTY output ║
║ Result: Undefined behavior, corrupted display ║
║ ║
╚═════════════════════════════════════════════════════════════════╝
Solution A: Mutex (Traditional)
Copy
╔═════════════════════════════════════════════════════════════════╗
║ MUTEX APPROACH ║
╠═════════════════════════════════════════════════════════════════╣
║ ║
║ Concept: "One lock guards the state, everyone waits" ║
║ ║
║ Thread A ───────▶ ┌──────────┐ ║
║ │ MUTEX │ ──────▶ Terminal State ║
║ Thread B ───────▶ │ (lock) │ ║
║ └──────────┘ ║
║ Thread C ───────▶ │ ║
║ │ ║
║ ┌────┴────┐ ║
║ │ WAITING │ Thread B, C blocked ║
║ └─────────┘ ║
║ ║
╚═════════════════════════════════════════════════════════════════╝
Problems with Mutex for Terminal
Copy
╔═════════════════════════════════════════════════════════════════╗
║ 1. LOCK CONTENTION ║
╠═════════════════════════════════════════════════════════════════╣
║ ║
║ PTY output: 1000 writes/sec ║
║ Renderer: 60 reads/sec ║
║ Each competes for same lock ║
║ ║
║ Timeline: ║
║ PTY: [LOCK──write──UNLOCK][LOCK──write──UNLOCK][LOCK...] ║
║ Render: [WAIT.......][LOCK──read──UNLOCK][WAIT...] ║
║ ║
║ Renderer starves or PTY blocks -> bad UX ║
║ ║
╚═════════════════════════════════════════════════════════════════╝
╔═════════════════════════════════════════════════════════════════╗
║ 2. PRIORITY INVERSION ║
╠═════════════════════════════════════════════════════════════════╣
║ ║
║ Low-priority resize holds lock ║
║ High-priority PTY output waits ║
║ Terminal feels sluggish ║
║ ║
╚═════════════════════════════════════════════════════════════════╝
╔═════════════════════════════════════════════════════════════════╗
║ 3. DEADLOCK RISK ║
╠═════════════════════════════════════════════════════════════════╣
║ ║
║ Multiple resources (state + socket + log) ║
║ Lock ordering mistakes -> deadlock ║
║ Debugging nightmare ║
║ ║
╚═════════════════════════════════════════════════════════════════╝
Solution B: Actor Pattern (MonoTerm)
Copy
╔═════════════════════════════════════════════════════════════════╗
║ ACTOR APPROACH ║
╠═════════════════════════════════════════════════════════════════╣
║ ║
║ Concept: "Single owner processes messages in order" ║
║ ║
║ ┌───────────────────────────────────┐ ║
║ │ MPSC CHANNEL │ ║
║ │ (Multiple Producer, Single Consumer) │ ║
║ └────────────────┬──────────────────┘ ║
║ │ ║
║ PTY Thread ────▶ tx.send(PtyData) ─┤ ║
║ │ ║
║ Input Handler ──▶ tx.send(Input) ──┼───▶ rx.recv() ║
║ │ │ ║
║ Resize Handler ─▶ tx.send(Resize) ─┤ ▼ ║
║ │ ┌───────────────┐ ║
║ Close Handler ──▶ tx.send(Close) ──┘ │ SessionActor │ ║
║ │ (SINGLE OWNER)│ ║
║ │ │ ║
║ │ * All State │ ║
║ │ * No locks │ ║
║ │ * Sequential │ ║
║ └───────────────┘ ║
║ ║
╚═════════════════════════════════════════════════════════════════╝
Why Actor Wins for Terminal
Copy
╔═════════════════════════════════════════════════════════════════╗
║ 1. NO LOCK CONTENTION ║
╠═════════════════════════════════════════════════════════════════╣
║ ║
║ Messages queue, Actor processes sequentially ║
║ No thread ever blocks another ║
║ ║
║ Timeline: ║
║ PTY: [send][send][send][send][send] ◀── Never blocks ║
║ Actor: [recv──process──][recv──process──][recv──...] ║
║ ║
╚═════════════════════════════════════════════════════════════════╝
╔═════════════════════════════════════════════════════════════════╗
║ 2. GUARANTEED ORDER ║
╠═════════════════════════════════════════════════════════════════╣
║ ║
║ MPSC channel preserves message order ║
║ State transitions are predictable ║
║ Easy to reason about ║
║ ║
╚═════════════════════════════════════════════════════════════════╝
╔═════════════════════════════════════════════════════════════════╗
║ 3. NO DEADLOCK POSSIBLE ║
╠═════════════════════════════════════════════════════════════════╣
║ ║
║ No locks = no deadlock ║
║ Single owner = no circular waits ║
║ ║
╚═════════════════════════════════════════════════════════════════╝
Side-by-Side Comparison
Scenario: PTY outputs 100 lines while user resizes windowCopy
╔═════════════════════════════════════════════════════════════════╗
║ MUTEX APPROACH ║
╠═════════════════════════════════════════════════════════════════╣
║ ║
║ T=0ms PTY: lock() ────────────────────────────────┬ ║
║ T=1ms Resize: lock() -> BLOCKED │ ║
║ T=2ms PTY: write line 1 │ ║
║ T=3ms PTY: write line 2 │ Resize ║
║ ... │ WAITS ║
║ T=50ms PTY: write line 50 │ ║
║ T=51ms PTY: unlock() ──────────────────────────────┘ ║
║ T=52ms Resize: lock() -> SUCCESS, but 50ms late! ║
║ T=53ms Resize: apply new dimensions ║
║ T=54ms Resize: unlock() ║
║ ║
║ Result: Resize delayed, possible visual glitch ║
║ ║
╚═════════════════════════════════════════════════════════════════╝
╔═════════════════════════════════════════════════════════════════╗
║ ACTOR APPROACH ║
╠═════════════════════════════════════════════════════════════════╣
║ ║
║ T=0ms PTY: tx.send(Line1) ────────────────────┬ ║
║ T=0ms Resize: tx.send(Resize) ────────────────┼─▶ Channel ║
║ T=1ms PTY: tx.send(Line2) ────────────────────┘ ║
║ ... (all sends complete immediately) │ ║
║ │ ║
║ T=0ms Actor: recv(Line1) -> process │ ║
║ T=1ms Actor: recv(Resize) -> apply dimensions │ ║
║ T=2ms Actor: recv(Line2) -> process with NEW dimensions ║
║ ... ║
║ ║
║ Result: Resize processed in order, no blocking, correct dims ║
║ ║
╚═════════════════════════════════════════════════════════════════╝
Comparison Table
| Aspect | Mutex | Actor |
|---|---|---|
| Blocking | Writers block readers | No blocking (async send) |
| Order | Race for lock | FIFO guaranteed |
| Deadlock | Possible | Impossible |
| Complexity | Lock ordering rules | Message types only |
| Debug | Hard (race conditions) | Easy (sequential log) |
| Resize UX | May delay or glitch | Smooth, in-order |
Pattern History
The Actor pattern is not Rust-specific. It has a 50+ year track record.Copy
╔═════════════════════════════════════════════════════════════════╗
║ ACTOR PATTERN TIMELINE ║
╠═════════════════════════════════════════════════════════════════╣
║ ║
║ 1973: Carl Hewitt proposes Actor Model ║
║ │ ║
║ ▼ ║
║ 1986: Erlang/OTP implements Actor for telecom ║
║ (99.9999999% uptime) ║
║ │ ║
║ ▼ ║
║ 2009: Scala Akka brings Actor to JVM ║
║ │ ║
║ ▼ ║
║ 2015: Go channels (CSP, similar concept) ║
║ │ ║
║ ▼ ║
║ 2020: Rust Tokio async channels ║
║ │ ║
║ ▼ ║
║ 2024: MonoTerm SessionActor ║
║ ║
║ The pattern is LANGUAGE-INDEPENDENT. ║
║ Rust just makes it easy with ownership + MPSC channels. ║
║ ║
╚═════════════════════════════════════════════════════════════════╝
SessionActor in MonoTerm
The SessionActor owns all terminal state. No locks needed.Copy
╔═════════════════════════════════════════════════════════════════╗
║ SESSIONACTOR STRUCTURE ║
╠═════════════════════════════════════════════════════════════════╣
║ ║
║ ┌───────────────────────────────────────────────────────────┐ ║
║ │ SessionActor │ ║
║ │ │ ║
║ │ sessions: HashMap<String, SessionState> ← Owns state │ ║
║ │ grid_workers: HashMap<String, Sender> ← Per-session │ ║
║ │ rx: Receiver<SessionCommand> ← Single consumer│ ║
║ │ tx: Sender<SessionCommand> ← Cloned to all │ ║
║ │ │ ║
║ └───────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌───────────────────────────────────────────────────────────┐ ║
║ │ SessionCommand (Message Types) │ ║
║ │ │ ║
║ │ * CreateSession { cols, rows } │ ║
║ │ * HandlePtyData { session_id, data } │ ║
║ │ * ResizeSession { session_id, cols, rows } │ ║
║ │ * CloseSession { session_id } │ ║
║ │ * ... │ ║
║ │ │ ║
║ └───────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌───────────────────────────────────────────────────────────┐ ║
║ │ The Actor Loop │ ║
║ │ │ ║
║ │ loop { │ ║
║ │ match rx.recv().await { │ ║
║ │ HandlePtyData { id, data } => { │ ║
║ │ // No lock - we OWN the state │ ║
║ │ sessions[id].process(data) │ ║
║ │ } │ ║
║ │ ResizeSession { id, cols, rows } => { │ ║
║ │ // No lock - sequential processing │ ║
║ │ sessions[id].resize(cols, rows) │ ║
║ │ } │ ║
║ │ } │ ║
║ │ } │ ║
║ │ │ ║
║ └───────────────────────────────────────────────────────────┘ ║
║ ║
╚═════════════════════════════════════════════════════════════════╝
Summary
Copy
╔═════════════════════════════════════════════════════════════════╗
║ ACTOR OVER MUTEX: KEY TAKEAWAYS ║
╠═════════════════════════════════════════════════════════════════╣
║ ║
║ 1. Mutex "works" but creates contention under high output ║
║ 2. Actor eliminates contention by design (single owner) ║
║ 3. FIFO ordering means predictable state transitions ║
║ 4. No locks = no deadlock = no debugging nightmares ║
║ 5. Pattern is 50+ years old, battle-tested in Erlang telecom ║
║ ║
║ ─────────────────────────────────────────────────────────── ║
║ ║
║ For terminal state: ACTOR > MUTEX ║
║ ║
║ But Actor alone does not solve high-output rendering... ║
║ -> See: Actor + ACK Synergy ║
║ ║
╚═════════════════════════════════════════════════════════════════╝