parkring 0.4.0

Bounded MPMC queues (Vyukov and SCQ), a Chase-Lev work-stealing deque and a work-stealing thread pool, with futex-based parking, verified with loom and Miri.
Documentation

parkring

CI MSRV 1.85 License: MIT

Concurrency primitives in Rust, each implemented from its paper, checked with the loom model checker and Miri, and benchmarked against the established crate for the job.

what it is compared with
LockFreeQueue Vyukov's bounded MPMC ring with spin-then-park waiting crossbeam ArrayQueue
ScqQueue Nikolaev's SCQ (DISC 2019): fetch-add claims, genuinely lock-free the Vyukov queue
BlockingQueue mutex + two condvars, the reference implementation
Worker / Stealer Chase-Lev work-stealing deque with weak-memory-correct fences crossbeam-deque
ThreadPool, join a work-stealing pool built from the pieces above Rayon

Waiting threads spin briefly, then park on a futex (futex(2) on Linux, __ulock on macOS; Condvar elsewhere), so an idle thread uses no CPU. The only dependency is libc.

use parkring::{LockFreeQueue, ThreadPool, join};

// A bounded queue with shutdown: consumers drain, then stop.
let queue = LockFreeQueue::new(1024);
std::thread::scope(|s| {
    let consumer = s.spawn(|| {
        let mut sum = 0u64;
        while let Ok(v) = queue.pop() {   // parks while empty
            sum += v;
        }
        sum                               // Err(PopError) once closed and drained
    });
    for i in 0..10_000 {
        queue.push(i).unwrap();           // parks while full
    }
    queue.close();
    assert_eq!(consumer.join().unwrap(), (0..10_000).sum());
});

// Fork-join parallelism on a work-stealing pool.
fn fib(n: u64) -> u64 {
    if n < 20 {
        return if n < 2 { n } else { fib(n - 1) + fib(n - 2) };
    }
    let (a, b) = join(|| fib(n - 1), || fib(n - 2));
    a + b
}
let pool = ThreadPool::new(4);
assert_eq!(pool.install(|| fib(25)), 75_025);

Install

[dependencies]
parkring = "0.4"

Runnable examples are in examples/: a multi-stage pipeline, parallel quicksort with join, a small work-stealing scheduler, and backpressure with graceful shutdown. Run one with cargo run --release --example pipeline.

Choosing a type

you want use
a bounded MPMC queue whose waiting threads sleep instead of spinning LockFreeQueue
a queue with a lock-free progress guarantee, and you accept lower throughput ScqQueue (64-bit targets)
the simplest correct queue, for reference or low traffic BlockingQueue
per-thread task deques for your own scheduler Worker / Stealer
fork-join parallelism (join, install, spawn) ThreadPool

When to use something else

parkring is small and heavily verified, but the established crates are better in several places, and the benchmarks show where:

  • Many producers or consumers at maximum throughput: crossbeam's ArrayQueue is 10 to 17% faster from 2 + 2 threads up, and much faster with many producers feeding one consumer.
  • A channel API (Sender/Receiver, disconnect on drop, select): use std::sync::mpsc, crossbeam-channel or flume. parkring's queues are shared by reference and shut down with close.
  • Parallel iterators, or fine-grained join at scale: use Rayon. It matches parkring's pool on coarse work and is faster on very fine-grained joins at 8 threads.
  • async code: parkring blocks threads; it has no async API yet.

What the verification found

The tests were written to fail on real bugs, and they did. Each item links to the write-up.

  • The original take-home submission failed spuriously in try_push, lost items at non-power-of-two capacities, and spun forever when idle (DESIGN.md §9).
  • A capacity-1 overwrite in the Vyukov ring, found by drop accounting and independently by proptest, which shrank it to capacity 1 (DESIGN.md §5).
  • A lost wakeup loom could not verify, because loom treats SeqCst accesses as AcqRel. The parking protocol was re-derived on read-modify-writes and release sequences, which loom does model, and the hot path got cheaper (DESIGN.md §4).
  • A hole in the SCQ paper's threshold bound: with more threads than capacity, an item could be stranded forever. A stress test hung 11 times in 40; the fix and the reasoning are in SCQ.md §4.
  • The classic Chase-Lev double take: remove either SeqCst fence and loom produces an element was taken twice: [0, 1, 1]. CI builds each mutant and requires that failure (DEQUE.md §3).
  • Two aliasing violations Miri caught and loom could not: retiring a deque buffer through Box::from_raw retags memory a thief may still be reading (DEQUE.md §6), and a latch's &self argument stayed protected while the waiting thread freed it (POOL.md).

Results

Apple M4, million items per second (higher is better) unless stated. Full tables and methodology: docs/BENCHMARKS.md.

parkring reference
queue, 1 producer + 1 consumer 91 (LockFreeQueue) 80 (crossbeam)
queue, 8 + 8 51 57 (crossbeam)
queue, 8 + 8, ScqQueue 7 51 (LockFreeQueue)
deque, one thief draining 85 70 (crossbeam-deque)
pool, fib(32) on 8 threads 1.36 ms 1.36 ms (Rayon)
parked consumer: wake latency / idle CPU 9.4 µs / 1.7% 8.8 µs / 1.3% (std sync_channel); 0.3 µs / 100% (crossbeam ArrayQueue, which never parks)

The losses are reported as plainly as the wins: crossbeam's queue is faster under contention, and SCQ, despite its stronger progress guarantee, is 5–8× slower than the Vyukov queue on this hardware. SCQ.md §7 profiles why.

Throughput scaling Wake latency against idle CPU Pool scaling

Verification

covers
loom every interleaving (up to a preemption bound) of the parking protocol, close races, both queues' claims, the deque's pop/steal races, and the pool's sleep/wake; three deliberately broken builds must fail
Miri the unsafe code in every component: uninitialised reads, double drops, leaks, aliasing, data races, and the real futex system call on Linux
proptest each queue against a VecDeque model (capacities 1–17), the deque against a VecDeque with wrapping indices
concurrency tests exactly-once delivery and per-producer FIFO across many shapes; per-thief ordering for the deque; repeated runs to flush out rare schedules
getrusage parked queues and an idle pool use about 35–55 µs of CPU over 300 ms
cargo test --workspace
RUSTFLAGS="--cfg loom" cargo test -p parkring --release --test loom --test loom_scq --test loom_pool
RUSTFLAGS="--cfg loom" cargo test -p parkring --release --lib deque
cargo +nightly miri test -p parkring --target x86_64-unknown-linux-gnu
cargo bench -p parkring-bench && cargo run -p parkring-bench --release --example plot

CI runs all of it on Linux, macOS and Windows, plus the MSRV, docs, a FreeBSD check, both parkers under loom, and the loom mutants.

Documentation

  • DESIGN.md: the Vyukov queue, parking, and closing.
  • SCQ.md: the fetch-add queue, its departures from the paper, and why it is slower here.
  • DEQUE.md: the work-stealing deque and its memory orderings.
  • POOL.md: the thread pool.
  • BENCHMARKS.md: methodology and every measurement.

Project history

This crate began as a take-home assignment, published as bounded_mpmc_queue: a mutex queue and a Vyukov queue. Version 0.2 audited that submission, fixed its bugs and added parking, shutdown and the verification suite. Version 0.3 renamed it to parkring and added futex parking, SCQ, the work-stealing deque and the pool. The git history shows each step.

Layout

src/
  queue/lockfree.rs     LockFreeQueue          queue/scq/     ScqQueue
  queue/blocking.rs     BlockingQueue          deque/         Worker, Stealer
  pool/                 ThreadPool, join       sync/futex/    futex backends
  sync/wait_queue/      parking                sync/primitives.rs  std/loom shim
tests/                  loom, proptest, drop accounting, regressions, CPU checks
crates/parkring-bench/  benchmarks and chart generation (unpublished)

License

Licensed under the MIT license.