Rust futures

Rust futures

innocentzero

2026-06-15

#rust #async #concurrency | Status: Ongoing

A bit on how rust futures work, and how it all pans out in the end.

How Rust futures work

Different types of concurrencies

From Callbacks to Promises

Simplifies the callback based approach. Instead of this:

setTimer(200, () => {
  setTimer(100, () => {
    setTimer(50, () => {
      console.log("I'm the last one");
    });
  });
});

we write this:

function timer(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}

timer(200)
.then(() => timer(100))
.then(() => timer(50))
.then(() => console.log("I'm the last one"));

A promise returns a state machine which can be in one of the three states: pending, fulfilled and rejected.

Now the same in await syntax looks like this:

async function run() {
    await timer(200);
    await timer(100);
    await timer(50);
    console.log("I'm the last one");
}

Each await is a yield point where it gives over control to the scheduler.

A future in rust - polling

A future in rust represents a pending operation. It has three phases:

A mental model of an async runtime

This is not a necessity. So keep it in mind that there is no requirement for runtimes to adhere to this model. But this mode of thinking helps.

There are the three parts we mentioned above, the reactor, executor, and futures. They work using an object called the Waker. This waker is the one responsible for telling the executor that a certain future is ready to run.

The await points act as yield points for the runtime, and the runtime determines how it can handle them.

Generators - stackless coroutines

This is the basis of async await syntax in Rust. The largest benefit is the fact that it allows for borrowing across await points, which the promise and callback framework didn't. These are implemented in Rust as state machines.

Futures are equivalent to generators. This section gives a motivation behind pinning memory. We'll show manual invocations of polling (in this case it's called yielding) and what's returned for each poll. Suppose we have the following:

let A = move || {
    let s = String::from("foo");
    let borrowed = &s;
    yield borrowed.len();
    println!("{borrowed}");
};

This will be divided into executable chunks for the executor to poll. In this case, the chunks would be start, yield1 and fin. They also represent the state of the generator.

When we poll on start, we create the string s and then the borrow, but we need to store them somewhere. So the generator state yield1 stores them as they're needed for the next chunk of invocation. The lifetime would be tied to the object itself, so it's self-referential. Now whenever we poll, we basically check the state, call the corresponding computation, and then change the state of self to reflect the upcoming computation. Since we're holding references to ourselves, we can't really change the location in memory. That's where the problem lies, and the Pin mechanism solves exactly that.

Pinning