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.
Utilities
Event
s for task notification.channel
s 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?
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:
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.