Announcing unsend

John Nunley · May 15, 2023

I’d like to introduce unsend: a thread-unsafe runtime for thread-unsafe people.

Most contemporary async runtimes are thread safe, as they are meant to be used in networking applications where multithreading is all but necessary. This kind of hardware parallelism improves the performance of parallel programs. However, you may want to avoid this kind of synchronization instead. Reasons for this include:

  • You are dealing with data that is !Send and therefore cannot be shared between threads.
  • You want to avoid including the standard library or the operating system.
  • You are running on embedded hardware that does not support multithreading.
  • You want to avoid the overhead of synchronization for programs that aren’t as parallel. For instance, if your process relies on heavily mutating shared data structures, synchronization may cause more harm than good.

This is the strategy that quite a few async runtimes outside of Rust take. The Redis database uses this strategy, as most of its work is I/O bound and thus not really improved by multithreading. Node.js is also single-threaded, largely for the same reason: JS programs are generally intended to be I/O bound, and thus multithreading is not necessary.

There are existing single-threaded executors in existing runtimes; tokio has LocalSet and smol has LocalExecutor. unsend aims to differentiate itself by using entirely thread-unsafe utilities. There are no atomics or mutexes in its channel implementation or synchronization primitives. Everything is done in RefCell and Rc, not Mutex and Arc.

Actualy, that’s not right. With executors, this becomes significantly more complicated. Waker needs to be Send + Sync, meaning that the internal scheduling function has to be thread safe. By default, the executor uses a thread-aware atomic channel to store tasks. However, if the std feature is enabled, the Waker can detect whether it was woken up from the same thread that it was created in. If this is the case, the executor will use a thread-unsafe channel instead.


Is it worth it?

In theory, unsend is faster than your average runtime for non-parallelizable workloads. I wanted to test this out for myself, so I wrote a simple benchmark. There are two programs: one is a basic “hello world” HTTP server while the other uses a centralized counter. The first one is easily parallelizable, while the second one would require shared data. I used the wrk utility to benchmark the two programs.

The results are as follows:

Hello world


So it turns out there isn’t much difference in real life. Ah well. I still think this crate is useful; especially for things like async-winit where thread-safety is a forgone conclusion and the overhead of synchronization is not worth it.

Twitter, Facebook