One page of async Rust
2026-02-17 19:42https://dotat.at/@/2026-02-16-async.html
I'm writing a simulation, or rather, I'm procrastinating, and this blog post is the result of me going off on a side-track from the main quest.
The simulation involves a bunch of tasks that go through a series of
steps with delays in between, and each step can affect some shared
state. I want it to run in fake virtual time so that the delays are
just administrative updates to variables without any real
sleep()ing, and I want to ensure that the mutations happen in the
right order.
I thought about doing this by representing each task as an enum
State with a big match state to handle each step. But then I
thought, isn't async supposed to be able to write the enum State and
match state for me? And then I wondered how much the simulation
would be overwhelmed by boilerplate if I wrote it using async.
Rather than digging around for a crate that solves my problem, I thought I would use this as an opportunity to learn a little about lower-level async Rust.
Turns out, if I strip away as much as possible, the boilerplate can fit on one side of a sheet of paper if it is printed at a normal font size. Not too bad!
But I have questions...
no subject
Date: 2026-02-17 22:38 (UTC)I will hopefully come back and read a little bit more about the specifics.
I *think* you know all this better than me, FWIW and/or to check my understanding, my impression of async rust has been:
* The thing that most people want async for is to write code with lots of little tasks that all work through some code interspersed with system calls. Rust does some super clever but obscure and a bit cheaty magic in how it defines Futures and poll() to make this viable.
* In practice, people can define async functions and don't need to think at all about the ways that the compiler uses futures in order to implement them.
* The thing that guides to rust async unforgiveably refuse to tell you is that although the rust language provides async functions, implemented in terms of futures, to do anything with them, you need an async executor library, which is not built into the language, and almost everyone uses tokio.
* The thing *I* (and Simon) most commonly want to do is to write functions as coroutines for clarity, but with no need for anything but running them in whatever order they want to run in on a one thread. As a spin-off of the async code, rust eventually (mostly) implemented a "yield" statement that turns a function into an iterator. Futures returned by async functions are implemented using the same mechanism, but with all the poll machinery that my code doesn't need.
Similarly to my experience, does your code actually benefit from the async framework? It feels like it should. But the effort with poll is to make it possible for the executor to listen to an OS callback to know when a task (might) be waking up from a syscall. But your code probably knows exactly which task to run, the task with the least "virtual time", something like that. And mediating your virtual time through the OS might add extra unnecessary weight. So you might be able to follow a "how to write an executor" guide and leave most of the poll functions pretty trivial. But maybe just a set of coroutine iterators sorted by time is all you need would be simpler?
A final rant, not specifically related to rust. I feel like the ".await" syntax is unnecessarily clunky -- could we not have ".await" as the default and a special syntax for "Return an unexecuted future, which I can put in an executor in a minute"...