Work-Stealing Scheduler
A lock-free parallel task scheduler using the Chase-Lev deque algorithm.
Architecture
┌─────────────────┐
│ Global Injector │ ← External task spawns
│ (MPMC Queue) │
└────────┬────────┘
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Worker 0 │ │ Worker 1 │ │ Worker 2 │
│ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │
│ │ Local │ │ │ │ Local │ │ │ │ Local │ │
│ │ Deque │ │◄──────►│ │ Deque │ │◄──────►│ │ Deque │ │
│ │(LIFO/FIFO)│ │ │(LIFO/FIFO)│ │ │(LIFO/FIFO)│
│ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
└──────────────────────┴──────────────────────┘
Work Stealing
(FIFO from top)
Components
Global Injector
MPMC (multi-producer, multi-consumer) queue for external task spawns.
pub struct Injector<T> {
buffer: Box<[AtomicCell<Option<T>>]>,
head: AtomicUsize, // Consumer position
tail: AtomicUsize, // Producer position
}
impl<T> Injector<T> {
pub fn push(&self, task: T) -> InjectResult;
pub fn steal(&self) -> Option<T>;
}
Worker Deque
Per-worker Chase-Lev deque. Owner pushes/pops from bottom (LIFO), thieves steal from top (FIFO).
pub struct Worker<T> {
buffer: Box<[AtomicCell<Option<T>>]>,
bottom: AtomicUsize, // Owner's position
top: AtomicUsize, // Thief's position
}
impl<T> Worker<T> {
pub fn push(&self, task: T); // O(1), owner only
pub fn pop(&self) -> Option<T>; // O(1), owner only
pub fn stealer(&self) -> Stealer<T>; // Get steal handle
}
pub struct Stealer<T> {
// Cloneable reference to worker's deque
}
impl<T> Stealer<T> {
pub fn steal(&self) -> StealResult<T>; // O(1), any thread
}
Usage
use axeberg::kernel::work_stealing::{WorkStealingExecutor, Config};
// Configure executor
let config = Config::default()
.num_workers(4)
.local_queue_capacity(256);
let executor = WorkStealingExecutor::new(config);
// Spawn tasks
executor.spawn(async { work_a().await });
executor.spawn(async { work_b().await });
// Run until completion
executor.run();
Scheduling Algorithm
Worker Loop:
┌─────────────────────────────────────────────────────────────┐
│ 1. Pop from local deque (LIFO - cache locality) │
│ └─ Found? Execute task, goto 1 │
│ │
│ 2. Steal from global injector │
│ └─ Found? Execute task, goto 1 │
│ │
│ 3. Steal from random peer worker (FIFO - load balance) │
│ └─ Found? Execute task, goto 1 │
│ └─ All empty? goto 4 │
│ │
│ 4. Park thread (wait for new work notification) │
│ └─ Woken? goto 1 │
└─────────────────────────────────────────────────────────────┘
Properties
Formally verified in TLA+ specification:
| Property | Description |
|---|---|
| W1: No Lost Tasks | Every spawned task is eventually executed |
| W2: No Double Execution | Each task executes exactly once |
| W3: LIFO/FIFO | Owner pops newest (cache), thieves steal oldest (balance) |
| W4: Linearizability | All operations appear atomic |
| W5: Progress | System makes progress under fair scheduling |
Lock-Free Guarantees
- Wait-free push/pop: Owner never blocks
- Lock-free steal: Thieves use CAS, no blocking
- ABA-safe: Generation counters prevent ABA problems
Memory Ordering
// Push (owner only)
buffer[bottom].store(task, Ordering::Relaxed);
bottom.fetch_add(1, Ordering::Release); // Publish to thieves
// Pop (owner only)
bottom.fetch_sub(1, Ordering::SeqCst); // Sync with steal
let task = buffer[bottom].take();
// Steal (any thread)
let t = top.load(Ordering::Acquire); // Read before buffer
let task = buffer[t].take();
top.compare_exchange(t, t+1, Ordering::SeqCst);
Configuration
pub struct Config {
/// Number of worker threads (default: num_cpus)
pub num_workers: usize,
/// Capacity of each worker's local deque (default: 256)
pub local_queue_capacity: usize,
/// Steal attempts before parking (default: 32)
pub steal_attempts: usize,
}
Performance Characteristics
| Operation | Complexity | Contention |
|---|---|---|
| Push | O(1) | None (owner only) |
| Pop | O(1) | Rare (owner vs stealer) |
| Steal | O(1) | Low (CAS retry) |
| Inject | O(1) | Moderate (MPMC) |
When to Use
| Context | Executor |
|---|---|
| Browser (WASM) | Single-threaded cooperative |
| Native CLI | Work-stealing |
| Server | Work-stealing |
| Tests | Either |
Example: Parallel Map
use axeberg::kernel::work_stealing::{WorkStealingExecutor, Config};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
let executor = WorkStealingExecutor::new(Config::default());
let results = Arc::new(Mutex::new(Vec::new()));
for i in 0..1000 {
let results = results.clone();
executor.spawn(async move {
let computed = expensive_compute(i);
results.lock().unwrap().push((i, computed));
});
}
executor.run();