Futures

In Rust, a Future is essentially "a value that will produce a result later".

  • Calling an async function does nothing but construct and return a future.

  • All local variables are stored in the function’s future, using an enum to identify where execution is currently suspended.

The Core: Future Trait

std::future - Rust

The Future trait in Rust defines the interface for asynchronous computations that will eventually produce a value or an error. This trait is part of the standard library and provides a common API for interacting with different types of futures. It has a single associated type, Output, which represents the type of the value that the future will eventually produce.

Here's a simplified definition of the Future trait:

type Output: The type of the value that the future will eventually produce (like Result<T, E> or u32)..

poll() → Called by the async runtime to make progress on the future.Futures can be advanced by calling the poll function, which will drive the future as far towards completion as possible.

Pin ensures that the Future isn’t moved in memory, so that pointers into that future remain valid. This is required to allow references to remain valid after an .await.

Recall an async function or block creates a type implementing Future and containing all of the local variables. Some of those variables can hold references (pointers) to other local variables. To ensure those remain valid, the future can never be moved to a different memory location.

To prevent moving the future type in memory, it can only be polled through a pinned pointer. Pin is a wrapper around a reference that disallows all operations that would move the instance it points to into a different memory location.

If the future completes, it returns Poll::Ready(result).

If the future is not able to complete yet, it returns Poll::Pending and arranges for the wake() function to be called when the Future is ready to make more progress.

When wake() is called, the executor driving the Future will call poll again so that the Future can make more progress.

Without wake(), the executor would have no way of knowing when a particular future could make progress, and would have to be constantly polling every future.

With wake(), the executor knows exactly which futures are ready to be polled.


2️⃣ The Poll Enum

poll() returns:

  • Poll::Ready → The work is complete, the Output is available.

  • Poll::Pending → Not ready yet; runtime should stop polling until woken.


3️⃣ Waker — The “Wake Me Up” Signal

When a future returns Poll::Pending, it must arrange for the runtime to wake it up later when progress is possible. This is done with a Waker.

  • Waker is provided inside the Context argument in poll().

  • When the future's underlying resource is ready (e.g., network socket has data), it calls:

This signals the runtime: "Please call poll() on me again soon".


4️⃣ The Event Loop (Executor/Runtime)

The runtime keeps a queue of tasks (each wrapping a future).

  1. Picks a task → calls poll()

  2. If Poll::Ready → remove it from queue (done)

  3. If Poll::Pending → store its Waker somewhere

  4. When the resource signals readiness → runtime calls Waker::wake() → put task back in queue

  5. Repeat until all futures are Ready


5️⃣ Relationships Diagram

Here’s the flow:

Step-by-step flow (how it actually runs)

1. executor dequeues a Task from the ready queue and calls poll() on its Future (wrapped inside Task).

2. If poll() returns Poll::Pending, the future must arrange to be woken later. Usually it:

  • Stores a clone of cx.waker() into some resource (e.g., socket, timer, channel).

  • Returns Pending. The executor does not keep polling this task until it is woken.

3. The future’s resource (e.g., OS socket) is monitored by the reactor / I/O driver (epoll/kqueue/IOCP). When that resource becomes ready (e.g., readable), the reactor calls the stored waker.

4. Waker::wake() enqueues the corresponding Task back into the executor’s ready queue (or sets a flag which causes it to be re-enqueued).

5. The executor will later dequeue that task again and call poll() again. This time the future can make progress (maybe return Ready(value) or Pending again).

6. If poll() returns Ready(value), the executor runs the completion logic (deliver the value to awaiting callers, drop the task, run continuations).

7. Repeat for many tasks concurrently — multiple tasks get polled in turn; only tasks that have been enqueued (initial spawn or woken) are polled.


6️⃣ Example Walkthrough

Let’s imagine an async TCP read:

  • First poll → socket empty → returns Pending, saves waker

  • Later → OS signals “socket readable” → runtime calls wake()

  • Runtime puts future back into queue

  • Next poll → socket has data → returns Ready

Reactor side (pseudo):


7️⃣ Key Relationships

  • Future trait → abstraction for async values.

  • Output → type returned when future is done.

  • poll() → drives the future forward.

  • Waker → mechanism to notify the runtime when the future can make progress.

  • Poll::Pending → pause until waker is triggered.

  • Poll::Ready → future is complete; return Output.

Good read : Asynchronous programming in Rust

Amit Nadiger

Polyglot(Rust🦀, C++ 11,14,17,20, C, Kotlin, Java), Embedded systems, Linux, Android TV,STB, Cas, Blockchain, Polkadot, UTXO, Substrate, Wasm, Proxy-wasm,Engineering management.

1d

Thank you Ajay Sontakke

Like
Reply
Ajay Sontakke

Agile Coach | Agile Transformation | SPC 6.0 | Professional Coaching to support cancer warriors

1d

Well written Amit! The flow of explanation and examples are neat. Good learning for coders. Recommend making videos too 🙂 Cheers!

Shalina Shaik

Full-Stack Developer | Java & Front-End Technologies

2d

𝗦𝗻𝗲𝘀𝘁𝗿𝗼𝗻 𝗦𝘆𝘀𝘁𝗲𝗺𝘀 𝗛𝗶𝗿𝗶𝗻𝗴 𝗳𝗼𝗿 𝗺𝘂𝗹𝘁𝗶𝗽𝗹𝗲 𝗧𝗲𝗰𝗵 𝗜𝗻𝘁𝗲𝗿𝗻𝘀𝗵𝗶𝗽𝘀 𝗮𝗽𝗽𝗹𝘆 𝗻𝗼𝘄 : https://www.snestronsystems.com/internships

Like
Reply

To view or add a comment, sign in

Explore topics