An async, fast synchronization primitives for task wakeup.
Diatomic Waker is similar to AtomicWaker in that it enables
concurrent updates and notifications to a wrapped Waker
. Unlike the latter,
however, it does not use spinlocks[^spinlocks] and is significantly faster, in
particular when the consumer needs to be notified periodically rather than just
once. It can in particular be used as a very fast, single-consumer [eventcount]
to turn a non-blocking data structure into an asynchronous one (see MPSC channel
receiver example).
This library is an offshot of Asynchronix, an ongoing effort at a high performance asynchronous computation framework for system simulation.
runtime on contention, which is in effect an executor-mediated spinlock.
Add this to your Cargo.toml
:
toml
[dependencies]
diatomic-waker = "0.1.0"
A multi-producer, single-consumer channel of capacity 1 for sending
NonZeroUsize
values, with an asynchronous receiver:
```rust use std::num::NonZeroUsize; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc;
use diatomic_waker::{WakeSink, WakeSource};
// The sending side of the channel.
struct Sender {
wake_src: WakeSource,
value: Arc
// The receiving side of the channel.
struct Receiver {
wake_sink: WakeSink,
value: Arc
// Creates an empty channel. fn channel() -> (Sender, Receiver) { let value = Arc::new(AtomicUsize::new(0)); let wakesink = WakeSink::new(); let wakesrc = wakesink.source(); ( Sender { wakesrc, value: value.clone(), }, Receiver { wake_sink, value }, ) }
impl Sender { // Sends a value if the channel is empty. fn trysend(&self, value: NonZeroUsize) -> bool { let success = self .value .compareexchange(0, value.get(), Ordering::Relaxed, Ordering::Relaxed) .isok(); if success { self.wakesrc.notify() }; success } }
impl Receiver {
// Receives a value asynchronously.
async fn recv(&mut self) -> NonZeroUsize {
// Wait until the predicate returns Some(value)
, i.e. when the atomic
// value becomes non-zero.
self.wakesink
.waituntil(|| NonZeroUsize::new(self.value.swap(0, Ordering::Relaxed)))
.await
}
}
```
This is a low-level primitive and as such its implementation relies on unsafe
.
The test suite makes extensive use of [Loom] to assess its correctness. As
amazing as it is, however, Loom is only a tool: it cannot formally prove the
absence of data races.
A distinguishing feature of diatomic-waker
is its use of two waker storage
slots (hence its name) rather than one. This makes it possible to achieve
lock-freedom in situations where waker registration and notification are
performed concurrently. In the case of concurrent notifications, even though one
notifier does hold a notification lock, others notifiers never block: they
merely request the holder of the lock to send another notification, which is a
wait-free operation.
Compared to atomic-waker
, dummy notifications (with no waker registered) are
much cheaper. The overall cost of a successful notification (registration +
notification itself) is also much cheaper in the common case where the
registered/unregistered waker is always the same, because the last waker is
always cached to avoid undue cloning. Quantitatively, the costs in terms of
atomic Read-Modify-Write (RMW) operations are:
atomic-waker
,atomic-waker
(this assumes 1 RMW for
Waker::wake_by_ref
, 2 RMWs for Waker::wake
, 1 RMW for Waker::clone
).atomic-waker
(same assumptions as above + 1 RMW for Waker::drop
); this is
typically only necessary for the very first registration,This software is licensed under the Apache License, Version 2.0 or the MIT license, at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.