Rust Async

Table of Contents

References
Preamble
Differences vs C#
struct std::task::Waker
struct std::task::Context<'w>
Trivial Async Fn Example

References

Preamble

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.

Differences vs C#

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.

struct std::task::Waker

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.

  1. Instead of invoking arc_fn(), you invoke waker.wake_by_ref()
  2. Alternatively, waker.wake() might be faster (but moves/uses waker)
  3. You can create a Waker from an Arc<impl Wake> (on nightly)
  4. You can create a Waker from an impl Fn() (via waker-fn)
  5. You can create a 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!

  6. RawWaker[VTable] allow you to implement a Waker without Arc/std/alloc (you might set a global atomic flag in wake/wake_by_ref, for example, in which case "clone" could just return the same static RawWakerVTable)

struct std::task::Context<'w>

This is just an overcomplicated, overengineered, future-proofed &'w Waker.

Construct with Context::from_waker(&waker).

Get the reference back out with context.waker().

Trivial Async Fn Example

Okay, so what the heck kind of magic complier pixie dust is behind async fns? 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".
				}
			}
		}
	}
}

TODO

— MaulingMonkey