nexus-async-rt 0.3.2

Single-threaded async executor with pre-allocated task storage
Documentation

nexus-async-rt

Single-threaded async runtime for latency-sensitive systems. Built on mio.

Not a tokio replacement — a purpose-built alternative for single-threaded event loops where predictable latency matters more than multi-threaded throughput.

When to use this vs tokio

Use nexus-async-rt when:

  • Single-threaded event loop (one core, no work-stealing)
  • Predictable tail latency (no scheduler jitter)
  • Zero-alloc task spawning on the hot path (slab allocation)
  • Futures are !Send — and that's fine
  • You're building on the nexus ecosystem (nexus-net, nexus-rt)

Use tokio when:

  • Multi-threaded execution or work-stealing
  • tokio ecosystem (tower, hyper, tonic, etc.)
  • Futures need to be Send across threads
  • Broad community support matters

Both can coexist in the same process. Use nexus-async-rt for the latency-critical event loop and tokio for everything else.

Quick start

use nexus_async_rt::*;
use nexus_rt::WorldBuilder;

let mut world = WorldBuilder::new().build();
let mut rt = Runtime::new(&mut world);

rt.block_on(async {
    let handle = spawn_boxed(async { 42 });
    let result = handle.await;
    assert_eq!(result, 42);
});

What you get

Task spawning

Two strategies, same API:

// Box-allocated — default, no setup needed
let handle = spawn_boxed(async { compute() });

// Slab-allocated — pre-allocated, zero-alloc hot path
let handle = spawn_slab(async { compute() });

Both return JoinHandle<T> — await for the result, drop to detach, or call abort() to cancel (consumes the handle).

Slab allocation (zero-alloc spawn)

For hot-path tasks where allocation jitter is unacceptable:

// SAFETY: single-threaded runtime owns the slab.
let slab = unsafe { Slab::<256>::with_chunk_capacity(64) };
let mut rt = Runtime::builder(&mut world)
    .slab_unbounded(slab)
    .build();

rt.block_on(async {
    // Pre-allocated — no Box, no allocator, zero syscalls
    let handle = spawn_slab(async { fast_path() });
    handle.await
});

Or claim a slot first, spawn later:

if let Some(claim) = try_claim_slab() {
    let handle = claim.spawn(async { work() });
    // ...
}

Timers

use std::time::Duration;

// Sleep
sleep(Duration::from_millis(100)).await;

// Timeout
let result = timeout(Duration::from_secs(5), some_future).await;

// Interval
let mut tick = interval(Duration::from_millis(10));
loop {
    tick.tick().await;
    poll_market_data();
}

I/O (mio-based)

use nexus_async_rt::{TcpStream, TcpListener, io};

// Client
let stream = TcpStream::connect(addr, io())?;

// Server
let listener = TcpListener::bind(addr, io())?;
let (stream, peer) = listener.accept().await?;

Channels

Three flavors for different use cases:

use nexus_async_rt::channel;

// Local MPSC — !Send, zero atomics, single-threaded
let (tx, rx) = channel::local::channel(64);

// Cross-thread MPSC — Sender: Clone + Send
let (tx, rx) = channel::mpsc::channel(64);

// Cross-thread SPSC — fastest cross-thread path
let (tx, rx) = channel::spsc::channel(64);

World access

Access nexus-rt World resources from async tasks:

with_world(|world| {
    let config = world.resource::<Config>();
    // ...
});

Graceful shutdown

rt.block_on(async {
    // ... spawn tasks ...
    shutdown_signal().await; // waits for Ctrl+C
});

Cancellation

let token = CancellationToken::new();
let child = token.child_token();

spawn_boxed(async move {
    while !child.is_cancelled() {
        do_work().await;
    }
    // cleanup
});

token.cancel(); // cancels all children

JoinHandle

spawn_boxed and spawn_slab return JoinHandle<T>:

  • Await — get the result: let val = handle.await;
  • Detach — drop the handle, task continues, output dropped on completion
  • Aborthandle.abort() consumes the handle, future dropped on next poll
  • Checkhandle.is_finished() for non-blocking status

JoinHandle is !Send and !Sync — stays on the executor thread.

Performance

Path p50
Task dispatch (poll cycle) 55-64 cycles
Local channel try_send+try_recv 13 ns
MPSC channel try_send+try_recv 22 ns
SPSC channel try_send+try_recv 15 ns
Cross-thread channel (busy spin) 15 ns
Cross-thread channel (park/epoll) 1.7 us
Tokio-compat waker bridge 76 ns

Features

Feature Default Description
tokio-compat No Adapters for bridging tokio and nexus-async-rt in the same process

Dependencies

  • mio — I/O event loop (epoll/kqueue)
  • nexus-rt — World/WorldBuilder for typed resource storage
  • nexus-slab — Optional pre-allocated task storage
  • nexus-timer — Hierarchical timer wheel
  • nexus-queue / nexus-logbuf — Lock-free internal queues

Platform support

Unix only (#![cfg(unix)]). Linux is the primary target, macOS supported.