blob: 003ea4789bcf3f14553bdce05a889eeea183ec37 [file]
.. _module-pw_async2-channels:
========
Channels
========
.. pigweed-module-subpage::
:name: pw_async2
Channels are the primary mechanism for communicating between asynchronous tasks
or threads in ``pw_async2``.
A channel is a fixed-capacity queue that supports multiple senders and multiple
receivers. Channels can be used between async tasks on the same dispatcher,
between tasks on different dispatchers, or between tasks and non-async code.
There are two types of channel: static channels, which have user-managed
storage, and dynamic channels which are allocated and automatically manage their
lifetimes.
.. _module-pw_async2-channels-create:
------------------
Creating a channel
------------------
Channels can be created in four configurations which control the number of
senders and receivers supported. The "multi-" configurations support up to 255
producers or consumers.
.. list-table:: Channel Configurations
:widths: 30 10 15 15
:header-rows: 1
* - Name
- Initials
- Max Producers
- Max Consumers
* - Single-producer, single-consumer
- SPSC
- 1
- 1
* - Single-producer, multi-consumer
- SPMC
- 1
- 255
* - Multi-producer, single-consumer
- MPSC
- 255
- 1
* - Multi-producer, multi-consumer
- MPMC
- 255
- 255
Examples of creating each channel type are shown below.
.. tab-set::
.. tab-item:: SPSC
.. code-block:: cpp
// Single-producer, single-consumer
#include "pw_async2/channel.h"
// Create storage for a static channel with a capacity of 10 integers.
// The storage must outlive the channel for which it is used.
pw::async2::ChannelStorage<int, 10> storage;
// Create a channel using the storage.
//
// In this example, we create a single-producer, single-consumer channel and
// are given the sole sender and receiver.
auto [channel, sender, receiver] = pw::async2::CreateSpscChannel(storage);
// Hand the sender and receiver to various parts of the system.
MySenderTask sender_task(std::move(sender));
MyReceiverTask receiver_task(std::move(receiver));
// You can hold onto the channel handle if you want to use it to
// manually close the channel before all senders and receivers have
// completed.
//
// If you want the channel to close automatically once either end hangs
// up, you should `Release` the handle immediately to disassociate its
// reference to the channel.
channel.Release();
.. tab-item:: SPMC
.. code-block:: cpp
// Single-producer, multi-consumer
#include "pw_async2/channel.h"
// Create storage for a static channel with a capacity of 10 integers.
// The storage must outlive the channel for which it is used.
pw::async2::ChannelStorage<int, 10> storage;
// Create a channel using the storage.
//
// In this example, we create a single-producer, multi-consumer channel and
// are given the sole sender. Receivers are created from the channel handle.
auto [channel, sender] = pw::async2::CreateSpmcChannel(storage);
// Hand the sender and receiver to various parts of the system.
MySenderTask sender_task(std::move(sender));
MyReceiverTask receiver_task_1(channel.CreateReceiver());
MyReceiverTask receiver_task_2(channel.CreateReceiver());
// You can hold onto the channel handle if you want to use it to
// manually close the channel before all senders and receivers have
// completed.
//
// If you want the channel to close automatically once either end hangs
// up, you should `Release` the handle after all desired receivers are
// created to disassociate its reference to the channel.
channel.Release();
.. tab-item:: MPSC
.. code-block:: cpp
// Multi-producer, single-consumer
#include "pw_async2/channel.h"
// Create storage for a static channel with a capacity of 10 integers.
// The storage must outlive the channel for which it is used.
pw::async2::ChannelStorage<int, 10> storage;
// Create a channel using the storage.
//
// In this example, we create a multi-producer, single-consumer channel and
// are given the sole receiver. Senders are created from the channel handle.
auto [channel, receiver] = pw::async2::CreateMpscChannel(storage);
// Hand the sender and receiver to various parts of the system.
MySenderTask sender_task_1(channel.CreateSender());
MySenderTask sender_task_2(channel.CreateSender());
MyReceiverTask receiver_task(std::move(receiver));
// You can hold onto the channel handle if you want to use it to
// manually close the channel before all senders and receivers have
// completed.
//
// If you want the channel to close automatically once either end hangs
// up, you should `Release` the handle after all desired senders are
// created to disassociate its reference to the channel.
channel.Release();
.. tab-item:: MPMC
.. code-block:: cpp
// Multi-producer, multi-consumer
#include "pw_async2/channel.h"
// Create storage for a static channel with a capacity of 10 integers.
// The storage must outlive the channel for which it is used.
pw::async2::ChannelStorage<int, 10> storage;
// Create a channel using the storage.
//
// In this example, we create a multi-producer, multi-consumer channel.
// Both senders and receivers are created from the channel handle.
pw::async2::MpmcChannelHandle<int> channel =
pw::async2::CreateMpmcChannel(storage);
// Hand the sender and receiver to various parts of the system.
MySenderTask sender_task_1(channel.CreateSender());
MySenderTask sender_task_2(channel.CreateSender());
MyReceiverTask receiver_task_1(channel.CreateReceiver());
MyReceiverTask receiver_task_2(channel.CreateReceiver());
// You can hold onto the channel handle if you want to use it to
// manually close the channel before all senders and receivers have
// completed.
//
// If you want the channel to close automatically once either end hangs
// up, you should `Release` the handle after all desired senders and
// receivers are created to disassociate its reference to the channel.
channel.Release();
.. _module-pw_async2-channels-handles:
---------------
Channel handles
---------------
Each channel creation function returns a handle to the channel. This handle is
used for two operations:
1. Creating senders and receivers, if allowed by the channel configuration
(``CreateSender``, ``CreateReceiver``).
2. Forcefully closing the channel while senders and receivers are still active
(``Close``).
Handles are movable and copyable, so they can be given to any parts of the
system which need to perform these operations.
As long as any handle to a channel is active, the channel will not automatically
close. If the system relies on the channel closing (for example, a receiving
task reading values until a ``std::nullopt``), it is essential to ``Release``
all handles once you are done creating senders/receivers from them.
.. _module-pw_async2-channels-txrx:
---------------------
Sending and receiving
---------------------
Senders and receivers provide asynchronous APIs for interacting with the
channel.
- ``Sender::Send(T value)``: Returns a ``Future<bool>`` which resolves to
``true`` when the value has been written to the channel. If the channel is
full, the future waits until space is available. If the channel closes, the
future resolves to ``false``.
- ``Receiver::Receive()``: Returns a ``Future<std::optional<T>>`` which waits
until a value is available, or resolves to ``std::nullopt`` if the channel is
closed and empty. Values written to the channel are distributed among pending
receivers.
.. tab-set::
.. tab-item:: Standard polling
.. literalinclude:: examples/channel.cc
:language: cpp
:linenos:
:start-after: // DOCSTAG: [pw_async2-examples-channel-manual]
:end-before: // DOCSTAG: [pw_async2-examples-channel-manual]
.. tab-item:: C++20 coroutines
.. literalinclude:: examples/channel.cc
:language: cpp
:linenos:
:start-after: // DOCSTAG: [pw_async2-examples-channel-coro]
:end-before: // DOCSTAG: [pw_async2-examples-channel-coro]
.. _module-pw_async2-channels-reservesend:
ReserveSend
===========
``Sender::ReserveSend()`` is an alternative API for writing data to a channel.
Unlike the regular ``Send``, which takes a value immediately and stages it in
its future, ``ReserveSend`` allows writing a value directly into the channel
once space is available. This can be useful for values which are expensive to
construct/move or rapidly changing. By waiting for a reservation, you can defer
capturing the value until you are guaranteed to be able to send it immediately.
``ReserveSend`` returns a ``Future<std::optional<SendReservation<T>>>``. The
``SendReservation`` object is used to emplace a value directly into the channel.
If the reservation is dropped, it automatically releases the channel space.
If the channel closes, the future resolves to ``std::nullopt``.
It is possible to use both ``Send`` and ``ReserveSend`` concurrently on the same
channel.
.. tab-set::
.. tab-item:: Standard polling
.. code-block:: cpp
using pw::async2::ReserveSendFuture;
using pw::async2::Sender;
class ReservedSenderTask : public pw::async2::Task {
public:
explicit ReservedSenderTask(Sender<int>&& sender)
: sender(std::move(sender)) {}
private:
Poll<> DoPend(pw::async2::Context& cx) override {
// Reserve space for a value in the channel.
if (!reservation_future_.is_pendable()) {
reservation_future_ = sender.ReserveSend();
}
PW_AWAIT(auto reservation, reservation_future_, cx);
if (!reservation.has_value()) {
PW_LOG_ERROR("Channel is closed");
return;
}
// Emplace a value into the channel.
reservation->Commit(42);
return pw::async2::Ready();
}
Sender<int> sender;
ReserveSendFuture<int> reservation_future_;
};
.. tab-item:: C++20 coroutines
.. code-block:: cpp
using pw::async2::Coro;
using pw::async2::CoroContext;
using pw::async2::Sender;
Coro<Status> ReservedSenderExample(CoroContext&, Sender<int> sender) {
// Wait for space to become available.
auto reservation = co_await sender.ReserveSend();
if (!reservation.has_value()) {
PW_LOG_ERROR("Channel is closed");
co_return pw::Status::FailedPrecondition();
}
// Emplace a value into the channel.
reservation->Commit(42);
co_return pw::OkStatus();
}
.. _module-pw_async2-channels-lifetime:
----------------
Channel lifetime
----------------
A channel remains open as long as it has at least one active handle, or at least
one active sender and one active receiver.
- If any handle is active, the channel remains open, even if there are no
senders or receivers.
- If there is no handle and all receivers are destroyed, the channel closes.
Subsequent ``Send`` attempts will fail (the future resolves to ``false``).
- If there is no handle and all senders are destroyed, the channel closes.
Subsequent ``Receive`` calls will drain any remaining items, then resolve to
``std::nullopt``.
.. _module-pw_async2-channels-alloc:
------------------
Dynamic allocation
------------------
In systems that have dynamic allocation, you can pass an :cc:`Allocator` and
channel capacity to any of the channel creation functions to allocate a managed
channel.
The dynamic functions wrap the returned tuple in a ``std::optional``. If the
allocation fails, the optional will be empty.
.. code-block:: cpp
#include "pw_async2/channel.h"
constexpr size_t kCapacity = 10;
auto result =
pw::async2::CreateSpscChannel<int>(GetSystemAllocator(), kCapacity);
if (!result.has_value()) {
PW_LOG_ERROR("Out of memory");
return;
}
// Hand the sender and receiver to various parts of the system.
auto&& [channel, sender, receiver] = *result;
// As with the static channel, release the handle once all desired senders
// and receivers are created unless you intend to use it to manually close
// the channel. As we created an SPSC channel here, there are no more senders
// or receivers so we release the handle immediately.
//
// The channel remains allocated and open as long as any senders, receivers,
// or futures to it are alive.
channel.Release();
.. _module-pw_async2-channels-sync:
------------------
Synchronous access
------------------
It is possible to interact with channels from a non-async context, such as a
separate thread or an interrupt handler. In these cases, use the following
methods:
- :cc:`Sender::TrySend`: Attempts to send the value immediately. Returns
a :cc:`pw::Status` indicating success.
- :cc:`Sender::TryReserveSend`: Attempts to reserve a slot in the channel
immediately. Returns a ``std::optional<SendReservation<T>>`` which contains
a reservation if successful, or ``std::nullopt`` if the channel is full or
closed.
- :cc:`Sender::BlockingSend`: Blocks the running thread until the value is sent
or an optional timeout elapses. Returns a status indicating success or
whether the channel is closed or the operation timed out.
- :cc:`Receiver::TryReceive`: Attempts to read a value from the channel
immediately. Returns a ``pw::Result<T>`` containing the value if successful,
or an error if the channel is empty or closed.
- :cc:`Receiver::BlockingReceive`: Blocks the running thread until a value is
received or an optional timeout elapses. Returns a ``pw::Result<T>``
containing either the value read or the error in case of timeout or channel
closure.
.. _module-pw_async2-notification-channels:
---------------------
Notification Channels
---------------------
A notification channel is a specialization of the channel types created when
the specified type is ``void``. Notification channels use somewhat less memory
than other channel types, as instead of a queue of `T` there is instead a single
counter with the count of notifications in the queue.
They are useful if all you need is a way of signalling to another task or
thread that something has happened, such as a generic completion event with no
other details.
Since you cannot send or receive a ``void`` data type, there are some minor
adjustments needed around sending or receiving data. The API differences
section below summarizes them. Other than these differences, notification
channels behave exactly like their regular counterparts, to make it easy to
switch to and from them.
Notification Channel API differences
====================================
* - :cc:`Sender::Send`
For a notification, there is no value argument. The returned
:cc:`SendFuture` has the same behavior and has no API changes.
.. code-block:: cpp
Sender<int> sender = /* ... */;
SendFuture<int> send_future = sender.Send(123); // Sends '123'
PW_TRY_READY_ASSIGN(bool sent, send_future.Pend(cx));
Sender<void> sender = /* ... */;
SendFuture<void> send_future = sender.Send(); // Sends a notification
PW_TRY_READY_ASSIGN(bool sent, send_future.Pend(cx));
* :cc:`Sender::TrySend`
For a notification, there is no value argument. The returned
:cc:`pw::Status` still indicates if the operation succeeded or not.
.. code-block:: cpp
Sender<int> sender = /* ... */;
pw::Status status = sender.TrySend(123); // Tries to send '123'
if (!status.ok()) {
/* handle failure */
}
Sender<void> sender = /* ... */;
pw::Status status = sender.TrySend(); // Tries to send a notification
if (!status.ok()) {
/* handle failure */
}
* :cc:`Sender::BlockingSend`
For a notification, there is no value argument, but there are still
``dispatcher`` and (optional) ``timeout`` arguments.
.. code-block:: cpp
Sender<int> sender = /* ... */;
pw::Status status = sender.BlockingSend(123, dispatcher, timeout);
if (!status.ok()) {
/* handle failure */
}
Sender<void> sender = /* ... */;
pw::Status status = sender.BlockingSend(dispatcher, timeout);
if (!status.ok()) {
/* handle failure */
}
* :cc:`SendReservation::Commit`
For a notification, use ``CommitNotification()`` instead, as ``Commit``
allows for in-place construction, and can take zero arguments to
default-construct your value.
The name change ensures that if you switch from a notification channel to a
value channel, the ``CommitNotification()`` call on a non-notification
channel will result in a build error ensuring you use a useful value.
.. code-block:: cpp
SendReservation<int> reservation = /* ... */;
reservation.Commit(123);
SendReservation<void> reservation = /* ... */;
reservation.CommitNotification();
* :cc:`Receiver::TryReceive`
For a notification, the return type changes from ``pw::Result<T>`` to
``pw::Status`` since there is no value to unwrap on success.
.. code-block:: cpp
Receiver<int> receiver = /* ... */;
pw::Result<int> result = receiver.TryReceive();
if (!result.ok()) {
/* handle failure */
}
Receiver<void> receiver = /* ... */;
pw::Status result = receiver.TryReceive();
if (!result.ok()) {
/* handle failure */
}
* :cc:`Receiver::BlockingReceive`
For a notification, the return type changes from ``pw::Result<T>`` to
``pw::Status`` since there is no value to unwrap on success.
.. code-block:: cpp
Receiver<int> receiver = /* ... */;
pw::Result<int> result = receiver.BlockingReceive(dispatcher);
if (!result.ok()) {
/* handle failure */
}
Receiver<void> receiver = /* ... */;
pw::Status result = receiver.BlockingReceive(dispatcher);
if (!result.ok()) {
/* handle failure */
}
* :cc:`ReceiveFuture::Pend()<pw::async2::ReceiveFuture>`
For a notification, the returned poll type changes from ``Poll<std::optional<T>>``
to ``Poll<bool>``. The contained bool is true if a notification was received,
or false if the channel was closed.
.. code-block:: cpp
ReceiveFuture<int> receive_future = /* ... */;
PW_TRY_READY_ASSIGN(std::optional<int> value, receive_future.Pend(cx));
if (!value) {
/* handle channel closed */
}
ReceiveFuture<int> result = /* ... */;
PW_TRY_READY_ASSIGN(bool value, receive_future.Pend(cx));
if (!value) {
/* handle channel closed */
}
-----------
Size report
-----------
See the :ref:`pw_async2 channels size report
<module-pw_async2-channels-size-report>` for code size information.