Internals
Internals

Async runtime & Pin

An async fn compiles to a state-machine struct that implements Future. An executor polls that state machine to completion. Pin enforces the rule that self-referential state machines cannot move. Tokio provides one such executor; it's where the runtime in "async runtime" lives.

Long read · from async/await desugaring through to tokio internals


1 · What async fn becomes

The compiler rewrites an async fn into a struct that holds local variables as fields and a state enum that records which .await point execution paused at. The struct implements Future by stepping forward each time it's polled.

rust src/main.rs · roughly what the compiler emits
async fn count() -> i32 {
    let a = read_one().await;
    let b = read_one().await;
    a + b
}

// Roughly compiles to:
enum CountState {
    Start,
    AwaitingA(ReadOneFut),
    AwaitingB { a: i32, fut: ReadOneFut },
    Done,
}

struct CountFuture {
    state: CountState,
}

impl Future for CountFuture {
    type Output = i32;
    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<i32> {
        let me = unsafe { self.get_unchecked_mut() };
        loop {
            match me.state {
                CountState::Start => {
                    me.state = CountState::AwaitingA(read_one());
                }
                CountState::AwaitingA(ref mut fut) => {
                    match unsafe { Pin::new_unchecked(fut) }.poll(cx) {
                        Poll::Pending => return Poll::Pending,
                        Poll::Ready(a) => {
                            me.state = CountState::AwaitingB { a, fut: read_one() };
                        }
                    }
                }
                // ...
            }
        }
    }
}

2 · The Future trait

rust core::future::Future
pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

pub enum Poll<T> {
    Ready(T),
    Pending,
}

A future is a value you can poll. Each poll either returns Ready(T) (the work is done) or Pending (the work isn't done and the future has arranged to be polled again later). The Context carries a Waker — the handle the future uses to ask the executor to poll it again.

3 · Why Pin

The state machine can hold references into its own fields. A local let a = ... followed by fn f(&a) compiles to a struct where one field holds a and another holds &a. If you moved that struct in memory, the &a field would point to the old location.

rust src/main.rs · Pin in shape
use std::pin::Pin;

// Pin<P> is a pointer type that promises:
//   "the pointed-to value will not move until it's dropped"

// Future::poll takes Pin<&mut Self> because once a Future starts
// being polled, it cannot move — moving could invalidate the
// self-references inside its state struct.
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
    // safe: we promise not to move *self
}
Unpin. Types without self-references implement the Unpin auto-trait, meaning Pin<&mut T> is no stronger than &mut T for them. Most futures generated by async fn are not Unpin — hence the dance with Box::pin, pin!, and tokio::pin!.

4 · The executor

The runtime — tokio, async-std, smol — provides three things: an executor that polls futures, a reactor that turns I/O events into Waker calls, and timers. The skeleton loop is conceptually:

rust executor.rs · the conceptual loop
loop {
    while let Some(task) = ready_queue.pop() {
        match task.future.as_mut().poll(&mut cx) {
            Poll::Ready(_) => { /* task complete; drop it */ }
            Poll::Pending  => { /* waker will re-queue when ready */ }
        }
    }
    reactor.wait_for_event();    // epoll / kqueue / IOCP
}

Tokio's real implementation adds per-thread queues, work stealing, scheduling fairness, and integration with timers — but the core is "poll until pending, sleep until woken".

5 · Wakers — the glue

When a future returns Pending, it must have arranged for a Waker to be called later. For an I/O future, the runtime's reactor (an epoll wrapper) registers interest in a file descriptor and, when readiness arrives, calls waker.wake() — which puts the task back on the ready queue.

rust src/main.rs · custom future
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct Ready(bool);

impl Future for Ready {
    type Output = i32;
    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<i32> {
        if self.0 {
            Poll::Ready(42)
        } else {
            // We can't make progress yet. Schedule a wake-up.
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}

6 · What blocks the runtime

  • Sync I/O. std::fs::read_to_string blocks the executor thread. Use tokio::fs or wrap in spawn_blocking.
  • CPU loops. An async fn that doesn't .await never yields. Other tasks starve.
  • Mutex held across .await. Risk of deadlock if the holder is suspended. Either drop before await, or use tokio::sync::Mutex.
  • Blocking syscalls. std::thread::sleep in async code blocks. tokio::time::sleep doesn't.

7 · Multi-threaded vs single-threaded runtimes

Tokio offers two flavours. The single-threaded runtime is one event loop on one thread; futures don't need to be Send. The multi-threaded runtime can move tasks between worker threads via work-stealing — futures must be Send so they can cross threads. The compiler enforces this at the spawn call site.

Single-threadedMulti-threaded
One worker threadN worker threads (default: num CPUs)
Tasks need not be SendTasks must be Send
Lower overhead per taskHigher overhead, but parallel
Best for I/O-only workloadsBest for I/O + light CPU mixes

8 · Debugging async code

  • tokio-console. Live view of tasks, their state, and how long they've been pending. Catches starved or stuck tasks.
  • tracing + tracing-subscriber. Async-aware spans. Each .await appears in the trace with timing.
  • cargo expand. Dumps the desugared state machine. Useful when you can't tell what a future is doing.
  • tokio::time::timeout. Wrap suspect work; if a future doesn't complete in N seconds, you know something's wrong.

References

Found this useful?