๐Ÿฆ€ Functional Rust

346: Runtime Context

Difficulty: 3 Level: Advanced The runtime is the engine behind async Rust โ€” a `Handle` lets you interact with it from outside async code.

The Problem This Solves

When you write `#[tokio::main]`, tokio creates a runtime, starts a thread pool, and runs your async code inside it. But sometimes you need to interact with the runtime from synchronous code โ€” spawn a task from a callback, block on a future from a library that doesn't expose async, or shut down the runtime cleanly after all work is done. The `Handle` pattern solves this: a `Handle` is a cheaply cloneable reference to a running runtime. You can spawn tasks on it, block on futures from sync code (`Handle::block_on`), or pass it to other threads. This is what `tokio::runtime::Handle::current()` does โ€” it returns a handle to the runtime that's currently driving the current async context. This example builds a minimal runtime (a worker thread + a channel for tasks) to illustrate the concept without requiring tokio.

The Intuition

Think of the runtime as a web server's event loop (like Node.js's libuv). A `Handle` is like an `EventEmitter` reference you can pass around โ€” it lets external code post work to the loop. In Python: `loop = asyncio.get_event_loop()` gives you a handle to submit coroutines from sync code with `loop.run_until_complete(coro)`. In tokio: `tokio::runtime::Handle::current().spawn(async { ... })` submits a task from sync code to the running async runtime.

How It Works in Rust

struct Handle {
 sender: mpsc::SyncSender<Box<dyn FnOnce() + Send>>,
}

impl Handle {
 // Submit sync work to the runtime thread
 fn spawn_sync(&self, f: impl FnOnce() + Send + 'static) {
     let _ = self.sender.send(Box::new(f));
 }

 // Block the calling thread until the runtime thread completes the work
 fn block_on_simple<T: Send + 'static>(&self, f: impl FnOnce() -> T + Send + 'static) -> T {
     let (tx, rx) = mpsc::channel();
     self.spawn_sync(move || { let _ = tx.send(f()); });
     rx.recv().unwrap()  // wait for result
 }
}

// Graceful shutdown: signal the worker to stop after current task
fn shutdown(mut self) {
 *self.shutdown.lock().unwrap() = true;
 self.handle.spawn_sync(|| {});  // wake the worker thread
 if let Some(w) = self.worker.take() { w.join().unwrap(); }
}
The runtime worker thread loops on `rx.recv()`, executing tasks as they arrive. Dropping the sender (`drop(tx)`) causes `recv()` to return `Err`, cleanly ending the loop. The shutdown flag ensures the worker stops after the current task.

What This Unlocks

Key Differences

ConceptOCamlRust
RuntimeLwt scheduler (implicit global)Explicit runtime (`tokio::Runtime`)
HandleN/A โ€” Lwt is global`tokio::runtime::Handle` (clonable reference)
Submit from sync`Lwt_main.run` (blocks)`handle.spawn(...)` or `handle.block_on(...)`
Worker thread modelSingle-threaded event loopMulti-threaded work-stealing (tokio default)
use std::sync::{Arc, Mutex, mpsc};
use std::thread;
use std::future::Future;
use std::task::{Context, Poll};

// Minimal runtime handle concept
struct Handle {
    sender: mpsc::SyncSender<Box<dyn FnOnce() + Send>>,
}

impl Handle {
    // Spawn a synchronous task on the runtime
    fn spawn_sync(&self, f: impl FnOnce() + Send + 'static) {
        let _ = self.sender.send(Box::new(f));
    }

    // Bridge: run an async-style function from sync context
    fn block_on_simple<T: Send + 'static>(&self, f: impl FnOnce() -> T + Send + 'static) -> T {
        let (tx, rx) = mpsc::channel();
        self.spawn_sync(move || {
            let _ = tx.send(f());
        });
        rx.recv().unwrap()
    }
}

struct Runtime {
    worker: Option<thread::JoinHandle<()>>,
    handle: Handle,
    shutdown: Arc<Mutex<bool>>,
}

impl Runtime {
    fn new() -> Self {
        let (tx, rx) = mpsc::sync_channel::<Box<dyn FnOnce() + Send>>(100);
        let shutdown = Arc::new(Mutex::new(false));
        let sd = Arc::clone(&shutdown);

        let worker = thread::spawn(move || {
            while let Ok(task) = rx.recv() {
                task();
                if *sd.lock().unwrap() { break; }
            }
        });

        Self {
            worker: Some(worker),
            handle: Handle { sender: tx },
            shutdown,
        }
    }

    fn handle(&self) -> &Handle { &self.handle }

    fn shutdown(mut self) {
        *self.shutdown.lock().unwrap() = true;
        self.handle.spawn_sync(|| {}); // wake worker
        if let Some(w) = self.worker.take() { w.join().unwrap(); }
    }
}

fn main() {
    let rt = Runtime::new();
    let handle = rt.handle();

    // Spawn tasks from sync context
    handle.spawn_sync(|| println!("Task 1 on runtime"));
    handle.spawn_sync(|| println!("Task 2 on runtime"));

    // Block on result
    let result = handle.block_on_simple(|| {
        thread::sleep(std::time::Duration::from_millis(5));
        42
    });
    println!("Got: {result}");

    rt.shutdown();
    println!("Runtime shut down");
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn block_on_returns_value() {
        let rt = Runtime::new();
        let r = rt.handle().block_on_simple(|| 99);
        assert_eq!(r, 99);
        rt.shutdown();
    }
    #[test]
    fn spawn_executes_task() {
        let rt = Runtime::new();
        let (tx, rx) = mpsc::channel();
        rt.handle().spawn_sync(move || { tx.send(1).unwrap(); });
        assert_eq!(rx.recv_timeout(std::time::Duration::from_millis(100)).unwrap(), 1);
        rt.shutdown();
    }
}
(* OCaml: thread-local context via Domain-local storage *)

let current_context : string option ref = ref None

let with_context name f =
  let prev = !current_context in
  current_context := Some name;
  let result = f () in
  current_context := prev;
  result

let get_context () =
  match !current_context with
  | Some name -> name
  | None -> "root"

let () =
  Printf.printf "Context: %s\n" (get_context ());
  with_context "worker-1" (fun () ->
    Printf.printf "Inside: %s\n" (get_context ());
    with_context "nested" (fun () ->
      Printf.printf "Nested: %s\n" (get_context ())
    );
    Printf.printf "Back to: %s\n" (get_context ())
  );
  Printf.printf "Back to: %s\n" (get_context ())