worm-blossom

worm-blossom

The endeavours of Aljoscha Meyer and Sam Gwilym.

Frugal Async Rust

Async Rust can be quite complex. By adhering to certain restrictions, however, you can eliminate some incidental complexity:

On Cancel-Safety

A value is cancel-safe if you can call one of its async methods, drop the returned future without polling it to completion, and then know that the original value is still in a well-defined state.

In synchronous code, methods always run to completion. As the author of a method, you do not need to worry about only half of your code running and then being stopped by an outside force. If that was the case, every single method mutating more than a single piece of state would become a nightmare to keep consistent. But that is essentially what writing cancel-safe methods is like — at every single await, all internal state must be a valid starting state for any method being called.

Frugal Async Rust lifts the burden of writing cancel-safe methods from all method authors. The price to pay is that method callers must run every async method completion before calling the next method. See this discussion for an overview of the kind of trouble this prevents. Whereas that discussion focusses on how to ensure cancel-safety and how to work around non-cancel-safe futures, we take the opposite approach and simply forego any reliance on cancel-safety from the start.

On Send-Bounds

Authors of async methods face a choice: should the future they return be Send? Frugal Async Rust provides a blanket answer: no Send bound necessary.

Futures which are Send can be run on multithreaded executors. But implementing them can be tricky. In the easiest case, a few judicious Arcs and Mutexes do the trick. But even when things are that simple, all users need to pay the performance penalty of those synchronisation primitives, even when running on a single-threaded executor.

The Rust standard library defaults to non-threadsafe types, leaving multi-threaded code as an opt-in optimisation detail. In our opinion, async Rust should follow the same logic. Async is fundamentally about concurrency, not about parallelisation. Parallelisation is a nice optimisation to perhaps opt into, but not central. We are not the only ones to feel that way.

On Panic-Handling

Rust is able to unwind a thread after a panic, catch the panic, and resume program execution. Ensuring correctness after catching a panic can be non-trivial, and it can pollute public APIs — see the notion of poisoning for Mutexes, for example.

Frugal Async Rust eliminates these issues by simply mandating that panics must not be caught.