4 releases (2 stable)

1.0.1 Apr 26, 2024
1.0.0 Apr 23, 2024
1.0.0-pre.1 Mar 3, 2024

#786 in Embedded development

MPL-2.0 license

180KB
1.5K SLoC

Handoff structure for lilos

This implements a synchronous rendezvous structure, which lets a task pass a value to another task without extra copies or reserving storage space.

This used to be part of the core lilos API, but was extracted during the process of finalizing the lilos 1.0 version. It is currently separate from lilos because its API is not cancel-safe.

Despite not being cancel-safe, it's still quite useful. See the module docs for more details.


lib.rs:

Mechanism for handing data from one task to another, minimizing copies.

This crate provides the Handoff abstraction for lilos.

There are two sides to a Handoff<T>, the sender and the receiver. When both the sender and receiver are ready, a single T gets transferred from the sender's ownership to the receiver's. In this case, "ready" means that either the sender or receiver was already blocked waiting for its peer when that peer arrived -- with both tasks waiting at the handoff, we can copy the data and then unblock both.

Because we don't need any sort of holding area for a copy of the T, a Handoff<T> is very small -- about the size of two pointers.

In computer science this is referred to as a rendezvous, but that's harder to spell than handoff.

Creating and using a Handoff

Because the Handoff itself contains no storage, they're cheap to create on the stack. You then need to split then into their Pusher and Popper ends -- these both borrow the Handoff, so you need to keep it around. You can then hand the ends off to other futures. A typical use case looks like this:

let mut handoff = Handoff::new();
let (push, pop) = handoff.split();
join!(data_producer(push), data_consumer(pop));

If you just want to synchronize two tasks at a rendezvous point, and don't need to move data, use Handoff<()>. It does the right thing.

Caveats and alternatives

Only one Pusher and Popper can exist at a time -- the compiler ensures this. This simplifies the implementation quite a bit, but it means that if you want a multi-party rendezvous this isn't the right tool.

If you would like to be able to push data and go on about your business without waiting for it to be popped, you want a queue, not a handoff. See the lilos::spsc module.

Note that none of these types are Send or Sync -- they are very much not thread safe, so they can be freely used across async tasks but cannot be shared with an interrupt handler. For the same reason, you probably don't want to attempt to store one in a static -- you will succeed with enough unsafe, but the result will not be useful! The queues provided in spsc do not have this limitation, at the cost of being more work to set up.

Cancel safety

Handoff is not strictly cancel-safe, unlike most of lilos. Concretely, dropping a push or pop future before it resolves can cause the loss of at most one data item.

While technically cancel-unsafe, this is usually okay given the way handoffs are used in practice. Please read the docs for Pusher::push and Popper::pop carefully or you risk losing data.

If the push and pop ends of the handoff are "long-lived," held by tasks that won't be cancelled (such as top-level tasks in lilos) and never used in contexts where the future might be cancelled (such as with_timeout), then you don't need to worry about that. This is not a property you can check with the compiler, though, so again -- be careful.

Dependencies

~2.5MB
~43K SLoC