A high-performance, asynchronous runtime-agnostic alternative to tokio::sync::watch.
This library is completely runtime-agnostic and does not rely on any specific scheduler. It can be used seamlessly in various asynchronous environments such as smol, and others(non-tested).
In most scenarios, its measured performance is superior to tokio's native implementation.
| Number of Subscribers | see::sync |
tokio::sync::watch |
Advantage |
|---|---|---|---|
| 1 | 84.9 µs | 213.8 µs | 2.3x faster |
| 4 | 350.6 µs | 388.6 µs | 11% faster |
| 16 | 1.52 ms | 1.14 ms | tokio is faster |
| 64 | 2.17 ms | 2.67 ms | 23% faster |
(Benchmark platform CPU: Intel Core i5-11300H. More details can be found in the code repository.)
The library offers two versions of channels, sync and unsync, with significant performance differences and different underlying components:
The synchronized version uses the following components for thread-safe operations:
parking_lot::RwLockevent-listenerstd::sync::atomic
This version is meant for multi-threaded scenarios where the channel needs to be sent across thread boundaries.
The unsynchronized version uses lighter-weight components for single-threaded use cases:
std::cell::RefCellstd::cell::Celllocal-eventstd::rc::Rc
This version is significantly faster in single-threaded scenarios because it avoids synchronization overhead.
Benchmarks, run on a single thread, highlight the concrete trade-offs:
- Throughput (
spsc): Theunsyncchannel can process a high volume of messages approximately 8 times faster than itssynccounterpart (~4.8 µs vs. ~38.6 µs for 1,000 messages). - Access & Latency (
borrow,full_cycle): For individual operations like borrowing the current value or completing a single send-receive cycle, theunsyncversion demonstrates a latency that is about 4 times lower (~3.5 ns vs. ~14.3 ns for a borrow operation).
This performance gap stems from the fundamental design of each module. The sync version must use atomic operations and internal locking to guarantee thread safety (Send + Sync), which incurs a substantial overhead even in uncontested scenarios. In contrast, the unsync version relies on standard memory operations, making it drastically more efficient.
Therefore, the choice is clear:
- Use
see::unsyncfor maximum performance when all related asynchronous tasks are guaranteed to run on a single thread. This is the ideal choice for single-threaded runtimes likecompio. - Use
see::synconly when you explicitly need to share the channel between multiple OS threads and are willing to accept the associated performance cost.