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
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