1 unstable release

0.1.0 May 5, 2024

#2108 in Embedded development

MPL-2.0 license

180KB
1.5K SLoC

Cheap value sharing and change notification for lilos

This crate provides the Watch<T> data structure for sharing data with update notifications. See the crate docs for details.


lib.rs:

A simple mechanism for sharing a piece of data and being notified if it changes.

This module provides Watch<T>, a data structure that lets you share some data of type T, update it from multiple producers, and efficiently notify multiple consumers when it changes.

A Watch<T> is particularly useful for things like configuration data, which are effectively "global" (shared by many tasks) but may need special handling of changes.

let shared_number = Watch::new(1234_u32);

You can create a receive handle to the Watch<T> by calling subscribe. This produces a Receiver<T> that allows its holder to inspect the shared data, and be notified when it changes.

let rcvr = shared_number.subscribe();
// A receiver only tracks changes made _after_ it's created:
assert_eq!(rcvr.is_changed(), false);

You can create a send handle to the Watch<T> by calling sender. This produces a Sender<T> that allows its holder to update the shared data.

let sender = shared_number.sender();

// Update the shared data:
sender.send(4567);

// Now the receiver sees a change:
assert_eq!(rcvr.is_changed(), true);

// We can inspect it and mark it as seen:
rcvr.glimpse_and_update(|value| assert_eq!(*value, 4567));

Code that wants to monitor changes to the data can use the Receiver::changed future to do so:

loop {
    rcvr.changed().await;
    rcvr.glimpse_and_update(|value| process(value));
}

Reentrancy and panics

It is possible to attempt to use handles for a single Watch<T> reentrantly if you try hard enough. The implementation checks for this and will panic. For instance, attempting to send a new value from inside the closure passed to Receiver::glimpse:

let shared = Watch::new(some_data);
let sender = shared.sender();
let rcvr = shared.subscribe();

// This line will panic at runtime.
rcvr.glimpse(|contents| sender.send(*contents));

It is perfectly safe to send or inspect a different Watch<T> instance from within one of these closures, just not the same one.

In practice it's pretty hard to do this accidentally, but, now you know.

Implementation

Specifically, the Watch<T> contains a change count. Each time its contents are updated by any Sender, the change count gets incremented.

Each Receiver has its own copy of the change count, reflecting what the count was at the last time that Receiver accessed the shared data. If the change count stored in the Watch is different from the one stored in the Receiver, then there is an update that hasn't been seen by its holder yet.

Because the Watch<T> only stores a single copy of the data and a counter, a Receiver may not see every update to the data. If multiple updates happen between checks, the Receiver will only ever see the last one. This keeps both the storage requirements, and the cost of updates, low.

Watch<T> contains a Notify internally, which it uses to efficiently wake tasks that are awaiting Receiver::changed.

Dependencies

~2.5MB
~41K SLoC