Skip to main content

Actor Over Mutex

For terminal state management, the Actor pattern eliminates the race conditions that Mutex can only mitigate.

The Problem: Concurrent State Access

╔═════════════════════════════════════════════════════════════════╗
║  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)

╔═════════════════════════════════════════════════════════════════╗
║  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

╔═════════════════════════════════════════════════════════════════╗
║  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)

╔═════════════════════════════════════════════════════════════════╗
║  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

╔═════════════════════════════════════════════════════════════════╗
║  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 window
╔═════════════════════════════════════════════════════════════════╗
║  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

AspectMutexActor
BlockingWriters block readersNo blocking (async send)
OrderRace for lockFIFO guaranteed
DeadlockPossibleImpossible
ComplexityLock ordering rulesMessage types only
DebugHard (race conditions)Easy (sequential log)
Resize UXMay delay or glitchSmooth, in-order

Pattern History

The Actor pattern is not Rust-specific. It has a 50+ year track record.
╔═════════════════════════════════════════════════════════════════╗
║  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.
╔═════════════════════════════════════════════════════════════════╗
║  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

╔═════════════════════════════════════════════════════════════════╗
║  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                                    ║
║                                                                 ║
╚═════════════════════════════════════════════════════════════════╝