I’d like to introduce
unsend: a thread-unsafe runtime for thread-unsafe people.
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
!Sendand 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;
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
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.
Events for task notification.
channels for sending data between tasks.
- Synchronization primitives like mutexes, read-write locks and semaphores. They operate on tasks instead of threads.
- An executor for running tasks.
Is it worth it?
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:
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.