Recipe

Tokio (Rust) Primer

Tokio is the de-facto async runtime for Rust. It powers high-throughput network services, multiplexed I/O, and concurrent task graphs. This primer walks through the three patterns Meridian users hit most: spawning tasks, structured concurrency with join!, and channel-based message passing for actor-style services.

1. Spawning a task

The simplest unit of concurrency in Tokio is a task spawned onto the multi-threaded runtime. Tasks are cheap green threads, scheduled cooperatively across worker threads. The runtime owns them until they complete or the runtime shuts down.

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        println!("hello from a task");
        42u32
    });
    let v = handle.await.unwrap();
    println!("got {v}");
}

2. Structured concurrency with join!

When you need several futures to resolve before continuing, reach for the tokio::join! macro. It polls all branches concurrently on the current task, returning a tuple once every branch is ready. Unlike spawn, join is cancellation-safe: dropping the outer future drops every branch.

  • Use join! for fan-out work that shares a lifetime.
  • Use try_join! when any branch can fail with the same error type.
  • Use select! to race futures and act on whichever resolves first.

3. Channels and the actor pattern

For long-lived services, model each component as a task that owns its state and communicates via tokio::sync::mpsc. This pattern keeps state local, avoids locks, and makes back-pressure explicit through bounded channel capacity. A typical Meridian worker uses one inbound mpsc for jobs and one oneshot per request for the reply.

let (tx, mut rx) = tokio::sync::mpsc::channel(32);
tokio::spawn(async move {
    while let Some(job) = rx.recv().await {
        handle(job).await;
    }
});
tx.send(Job::new()).await.unwrap();