swactor-development-history/cfuzz/CYCLE_03_THREAD_PARKING.md

52 lines
2.7 KiB
Markdown
Raw Normal View History

2026-02-23 05:07:59 +00:00
# Cycle 3: Thread Parking for Instant Worker Wakeup — Development History
> Commit: `acacc1b` · 4 files · 59 insertions, 10 deletions
---
## Motivation
Before this change, idle workers used `thread::sleep` with a fixed timeout to wait for new work. This meant an idle worker wouldn't notice new messages until its sleep timer expired — up to 1ms of unnecessary latency on the idle-to-active transition. Under bursty workloads, this sleep-based backoff wastes both time and power.
## Competitor Analysis
| Runtime | Idle Strategy | Wakeup Mechanism |
|---------|--------------|-----------------|
| Tokio | Parker state machine (notified/sleeping/empty) | `unpark()` via atomic CAS |
| Linux | NO_HZ adaptive ticks (stop tick when idle) | Interrupt on new work |
| Go | `notewakeup` / futex | OS-level wake |
| BEAM | Scheduler sleep + signal | Thread signal |
| **Swactor (before)** | **`thread::sleep(1ms)`** | **Timer expiry only** |
Tokio's parker uses a 3-state machine (notified → sleeping → empty) with atomic transitions. The key insight: `unpark()` is a **no-op** if the thread isn't parked, so callers pay zero cost on the hot path.
## Implementation
- Replaced `thread::sleep` with `thread::park_timeout` in worker run loop
- Workers register `thread::current()` via `OnceLock<Thread>` on startup
- `send_to` and `spawn` call `Thread::unpark()` on target worker after enqueuing work
- Cross-worker sends from `WorkerContext` also unpark the target
- Zero new dependencies — uses only `std::sync::OnceLock` + `std::thread::park_timeout`
**Key files modified:** `src/worker.rs`, `src/runtime.rs`, `src/delivery.rs`
## Design Decisions
- **`OnceLock<Thread>` for thread handle storage** — set-once semantics match the worker lifecycle (one thread per worker, never changes). Simpler than `Mutex<Option<Thread>>`.
- **`park_timeout` instead of `park`** — timeout ensures workers periodically wake even without explicit unpark, preventing permanent sleep if an unpark is missed.
- **Unpark on `send_to` and `spawn`** — these are the two operations that create work for a worker. The cost is a single atomic store (no-op if thread is already running).
- **No condvar** — `thread::park/unpark` is simpler and avoids the spurious wakeup complexity of condition variables. Tokio's parker validates this approach.
## Tests Added
1 new test (51 → 52 total):
- `mt_parked_worker_wakes_on_send` — verifies that a parked worker processes a message immediately after send (not after timeout)
## Result
- 52 tests pass
- All workspace crates compile
- Idle-to-active latency reduced from up to 1ms to near-zero
- No overhead on hot path — `unpark()` is a no-op when thread isn't parked