References |
Preamble |
Differences vs C# |
struct std::task::Waker |
struct std::task::Context<'w> |
Trivial Async Fn Example |
std::future::{Future, *}
std::task:::{Poll, Context, Waker, RawWaker, RawWakerVTable, *}
std::pin::{self, Pin}
std::marker::{PhantomPinned, Unpin}
Async/await and Futures fundamentally have nothing to do with system / OS threads.
You can use them from a single threaded browser tab, for example, with:
wasm_bindgen_futures::spawn_local(future)
Or you can use a futures::executor::LocalPool
's:
local_pool.spawner().spawn_local(future)
However, they're also quite handy for waiting for another thread or I/O to complete without blocking a thread. Rust async fns can also be conceptualized as running on green threads, with enough of the internals exposed to let libraries and users have full control over their execution.
In C#, an async function is immediately and syncronously executed on the calling thread, and proceeds until the first await is encountered. Execution will later continue based on the calling thread's ambient SynchronizationContext.
In Rust, an async fn executes nothing until poll(c)
ed,
and is simply run syncronously within said poll function - be that within a LocalPool's run function,
or a one of a ThreadPool's many threads, or
block_on's internal machinery.
NOTE WELL: std::task::Waker != std::task::Wake
Polling pending futures in a busy loop is wasteful of CPU and battery power.
As such, Rust allows executors to skip repeatedly polling futures until a "hey I'm ready to progress please poll me again" callback is executed.
std::task::Waker is that callback.
It could've been a simple Arc<dyn Fn()>
,
but for various reasons (including performance and no-alloc support), it's been overengineered a bit.
arc_fn()
, you invoke waker.wake_by_ref()
waker.wake()
might be faster (but moves/uses waker
)Waker
from an Arc<impl Wake
> (on nightly)Waker
from an impl Fn()
(via waker-fn)Waker
manually:
use std::task::*; let waker = unsafe { Waker::from_raw(RawWaker::new( ptr, // *const () &RawWakerVTable::new( | ptr: *const () | -> RawWaker { ...clone... } | ptr: *const () | -> () { ...wake.. } | ptr: *const () | -> () { ...wake_by_ref... } | ptr: *const () | -> () { ...drop... } ) ))};waker-fn's src/lib.rs is a good minimal example, under 70 LOC including docs!
This is just an overcomplicated, overengineered, future-proofed &'w Waker
.
Construct with Context::from_waker(&waker)
.
Get the reference back out with context.waker()
.
async fn
s? Well, let's take a look
at some hypothetical rewrites a compiler could do, to better understand!
// Given: fn event() -> EventFuture { ... } struct EventFuture { ... } impl Future for EventFuture { type Output = u32; ... } // This: async fn trivial() -> u32 { println!("Hello!"); // line 1 let e = event(); // line 2 let value = e.await; // line 3 println!("Got: {}", value); // line 4 return value * 2; // line 5 } // Could be rewritten by the compiler into this: fn trivial() -> impl Future<Output = u32> { TrivialFuture::NeverPolled enum TrivialFuture { NeverPolled, WaitingForE(EventFuture), // The compiler would be OK with an opaque `impl Future` Finished(PhantomPinned), // Prevent TrivialFuture from implementing Unpin } impl Future for TrivialFuture { type Output = u32; fn poll(mut self: Pin<&mut TrivialFuture>, ctx: Context) -> Poll<u32> { let _self = self.as_mut(); loop { match _self { TrivialFuture::NeverPolled => { println!("Hello!"); // line 1 let e = event(); // line 2 *_self = TrivialFuture::WaitingForE(e); // line 3 // continue immediately to WaitingForE via loop }, TrivialFuture::WaitingForE(e) => { // Self is Unpin, and we never move EventFuture, so this *should* be safe: let e = unsafe { Pin::new_unchecked(e) }; let value = match e.poll(ctx) { // line 3 Poll::Pending => return Poll::Pending, // waiting Poll::Ready(v) => value = v, // done! } println!("Got: {}", value); // line 4 *_self = TrivialFuture::Finished(PhantomPinned); // line 5 return Poll::Ready(value * 2); // line 5 }, TrivialFuture::Finished(_) => panic!("`trivial` future already consumed!"), // NOTE: The compiler doesn't *have* to panic - could loop {} instead! // Only real requirement is that it must at least be "sound". } } } } }
So just to take a second stab at the question:
why do futures get moved and therefore require pinning?Many futures cannot be moved while being polled - hence the need for pinning, to prevent code like this:
let f = some_async_fn(); if let Poll::Ready(result) = f.poll(ctx) { return result; } let f2 = f; // MOVES `f`, even though there might be pointers into f that would dangle! if let Poll::Ready(result) = f2.poll(ctx) { return result; } pending_futures_ill_check_later.push(f2); // MOVES `f2`, even though there might be pointers into f that would dangle!And then we allow code like this:let f : Pin<Box<_>> = Box::pin(some_async_fn()); if let Poll::Ready(result) = f.poll(ctx) { return result; } let f2 = f; // Moves the outer pin/box - but not the future it contains, which is now at a stable non-moving location on the heap somewhere if let Poll::Ready(result) = f2.poll(ctx) { return result; } pending_futures_ill_check_later.push(f2); // Again, just moves the outer pin/box, the future itself is at a stable non-moving location
— MaulingMonkey