Futures#
pw_async2: Cooperative async tasks for embedded
A Future is an object that represents the value of an asynchronous operation
which may not yet be complete. Upon completion, the future produces the result
of the operation, if it has one.
Futures are the core interface to pw_async2 asynchronous APIs.
Core concepts#
Futures operate using the
informed poll model on which
pw_async2 is built. This model is summarized below, but it is recommended to
read the full description for important background knowledge.
Future API#
Futures use a standard API. There is no Future class; futures are unique types
with a common interface, but no shared base. In C++20 and later,
pw::async2::Future is a C++ concept that describes the future interface.
A Future<T> exposes the following API:
A default constructor that initializes the future to an empty state. An empty future does not represent an asynchronous operation and is neither pendable nor complete.
A destructor that abandons the future so no further operations will access it.
value_type: Type alias for the value produced by the future;voidif the future produces no value. The FutureValue<Future> helper resolves to the future’svalue_type, but mapsvoidto ReadyType so that aFutureValue<Future>can always be instantiated and referenced.Poll<value_type> Pend(Context& cx): CallingPendadvances the asynchronous operation until no further progress is possible. Returns Ready if the operation completes. Otherwise, uses the provided Context to store a waker and returns Pending. The waker wakes the task whenPendshould be called again.bool is_pendable(): Returns whether the future represents an active asynchronous operation which can be pended.bool is_complete(): Returns whether the future has already completed and had its result consumed.
Futures are single-use and track their completion status. It is an error to poll a future after it has already completed.
pw_async2 provides a pw::async2::Future concept that future implementations
must satisfy. Futures do not share a common base class, but may use common
helpers such as FutureCore.
Ownership and lifetime#
Futures are owned by the caller of an asynchronous operation. The task that receives the future is responsible for storing and polling it.
The provider of a future must either outlive the future or arrange for the future to be resolved in an error state when the provider is destroyed.
Polling#
Futures are lazy and do nothing on their own. The task owning a future must poll
it to drive it to completion. Calling a future’s Pend function advances its
operation and returns a Poll containing one of two
values:
Pending(): The asynchronous operation has not yet finished. The value is not available. The task polling the future is be scheduled to wake when the future can make additional progress.Typically, your task should propagate a
Pendingreturn upwards to notify the dispatcher that it is blocked and should sleep.Ready(T): The operation has completed, and the value is now available.
Once a future returns Ready, its state is final. Attempting to poll it again
results in an assertion.
This polling model allows a single thread to manage many concurrent operations without blocking.
Completed future lifetime
Once a future yields Ready, it is considered
complete and its state is final. The async2 framework must be free to destroy
the future immediately following a Ready return without invalidating its
result.
When returning a value from Ready, it must not contain references to the
future or its internal state.
Composability#
The power of futures is their ability to compose to construct complex asynchronous logic from smaller building blocks.
Futures can be classified into two categories: leaf futures and composite futures. Leaf futures represent a specific asynchronous operation, such as a read from a channel, or waiting for a timer. They contain the required state for their operations and manage the task waiting on them.
Composite futures are built on top of other futures, combining their results
to build advanced asynchronous execution graphs. For example, a Join future
waits for multiple other futures to complete, returning all of their results at
once. Composite futures can be used to express complex logic in a declarative
way. For details on defining custom composite futures and async helper
functions, see Implementing a composite future.
Coroutine support#
Futures’ simple Pend API makes them easy to use with async2’s
coroutine adapter. You can co_await a
function that returns a future directly, automatically polling the future to
completion.
Working with futures#
Calling functions that return futures#
Consider some asynchronous call which produces a simple value on completion. Pigweed provides ValueFuture<T> for this common case. The async function has the following signature:
class NumberGenerator {
public:
ValueFuture<int> GetNextNumber();
};
You would write a task that calls this operation as follows:
class MyTask : public pw::async2::Task {
private:
pw::async2::Poll<> DoPend(pw::async2::Context& cx) override {
// The future begins in a default-constructed state, which is not
// pendable. Initialize it on the task's first run.
if (!future_.is_pendable()) {
future_ = generator_.GetNextNumber();
}
PW_AWAIT(int number, future_, cx);
PW_LOG_INFO("Received number: %d", number);
return pw::async2::Ready();
}
NumberGenerator generator_;
ValueFuture<int> future_;
};
pw::async2::Coro<void> MyCoroutineFunction(pw::async2::CoroContext,
NumberGenerator& generator) {
// Pigweed's coroutine integration allows futures to be awaited directly.
int number = co_await generator.GetNextNumber();
PW_LOG_INFO("Received number: %d", number);
}
Writing functions that return futures#
All future-based pw_async2 APIs have the signature
Future<T> DoThing(Args... args);
Where Future<T> is some concrete future implementation (e.g.
ValueFuture) which resolves to a value of type
T and Args represents any arguments to the operation.
When defining an asynchronous API, the function should always return a
Future directly — not a Result<Future> or
std::optional<Future>. If the operation is fallible, that should be
expressed by the future’s output, e.g. Future<Result<T>>.
This is necessary for proper composability. It makes using asynchronous APIs
consistent and enables higher-level futures which compose other futures to
function cleanly. Additionally, returning a Future directly is essential to
be able to work with coroutines: co_await can be used directly and will
resolve to a Result<T>.
Naming conventions#
Follow these conventions for naming functions that interact with pw_async2
futures.
Name functions that return futures for the operation represented by the future, rather than the future itself.
Yes: Function is named for the Read operation.
ReadFuture<T> Read();
No: Function is named for the future it returns.
ReadFuture<T> GetReadFuture();
Do not label future-returning functions as “async”. Asynchronicity is implied by the future return value.
No: Future-returning function is named as
Async.ReadFuture<T> AsyncRead();
Prefix non-blocking functions with
Tryto distinguish then from future-returning functions.Yes: Non-blocking function starts with
Try.std::optional<T> TryRead();
Prefix functions that block the current thread with
Blocking.Yes: Blocking function starts with
Blocking.std::optional<T> BlockingRead();
Signalling tasks#
Tasks often need to wait for a single event to occur. pw_async2 provides
Notification for this purpose. Tasks call
Wait to obtain a future that will resolve once the notifier calls
Notify. The future does not resolve to a value.
Multiple tasks can wait on the same notification and will all be woken when it
is triggered. A notification can be triggered from any context (async,
non-async, ISR). Notification is implemented using a
BroadcastValueProvider.
Persisting results across suspensions with FutureOrValue#
In manual polling state machines (e.g. within a Task::DoPend implementation), a task often needs to wait for multiple independent asynchronous operations to complete. Because futures are single-use and cannot be polled again after resolving to Ready, a task that yields to wait for remaining operations must store the results of any operations that completed early across subsequent suspension points.
Manually managing separate variables for each future and its resolved value requires significant boilerplate. FutureOrValue solves this by providing a single in-place container that holds either the active pending future or its resolved result:
While an operation is in progress,
FutureOrValueholds the pending future.When the future resolves to
Ready,FutureOrValueimmediately destroys the future (releasing any resources it held) and stores the resulting value in its place.Calling
Advance(cx)advances a pending future, returningtrueif the value is available orfalseif still pending. If the value has already been resolved,Advance(cx)returnstrueimmediately without polling.
Warning
FutureOrValue is designed only to be used as internal private member
state inside the final consumer task or composite future.
Never return ``FutureOrValue`` from APIs. A
FutureOrValueis not a future; it is storage. APIs must always return futures directly.Do not pass ``FutureOrValue`` around.
FutureOrValueshould never leave its owning task or composite future. Once a future is moved in, it should stay in place until the value is extracted.Do not use
FutureOrValuewhen results are consumed immediately upon resolution (store and poll the raw future directly instead).In C++20 coroutines, use Coro or combinators like Join instead, which automatically preserve state across suspension points without manual wrappers.
Remember that a FutureOrValue<F> is simply a convenience to avoid having
to store both F future_ and std::optional<F::value_type> value_.
If you were not otherwise going to store those, FutureOrValue is the
wrong thing for you.
States#
An instance of FutureOrValue is in one of three logical states:
Empty: The default state, or after the value has been extracted via
Take()or cleared viaReset(). No future is active and no value is stored.empty()returnstrue.Pending: A future has been assigned but has not yet resolved.
has_future()returnstrue.Ready: The future has resolved and the value is stored.
has_value()returnstrue.
Advancing multiple slots with PW_FOV_TRY_ADVANCE#
When managing multiple FutureOrValue members, calling Advance on each
individually with early returns can short-circuit, preventing later futures from
being polled and registering their wakers.
pw_async2 provides the PW_FOV_TRY_ADVANCE macro to poll multiple
FutureOrValue slots in a single statement without short-circuiting. If any
slot is not yet ready, the macro returns Pending() from the enclosing
function after ensuring all provided slots have been advanced.
Example#
class ConcurrentOperationsTask : public pw::async2::Task {
private:
pw::async2::Poll<> DoPend(pw::async2::Context& cx) override {
// Start operations if they haven't been started yet.
if (op_a_.empty()) {
op_a_ = DoWork(1);
}
if (op_b_.empty()) {
op_b_ = DoWork(2);
}
// Advance both slots without short-circuiting.
PW_FOV_TRY_ADVANCE(cx, op_a_, op_b_);
// Both values are now available.
pw::Status status_a = op_a_.Take();
pw::Status status_b = op_b_.Take();
if (status_a.ok() && status_b.ok()) {
PW_LOG_INFO("Both operations completed successfully");
}
return pw::async2::Ready();
}
pw::async2::FutureOrValue<ValueFuture<pw::Status>> op_a_;
pw::async2::FutureOrValue<ValueFuture<pw::Status>> op_b_;
};
Implementing a future#
pw_async2 provides futures like ValueFuture
for common asynchronous patterns. However, you may want to implement a custom
leaf future if your operation has complex logic where Pend() would benefit
from reaching deeper into the underlying system, e.g. waiting for a hardware
interrupt.
FutureCore is the primary tool for creating futures.
FutureCore#
This class provides the essential machinery for most custom leaf futures:
It stores the Waker of the task that polls it.
It manages its membership in an intrusive list of futures.
It tracks future state with a FutureState.
Future implementations typically have a FutureCore member.
FutureList#
After you vend a future from an asynchronous operation, you need a way to track and resolve it once the operation has completed. FutureCores can be stored in a FutureList, which wraps an pw::IntrusiveForwardList.
FutureList allows multiple concurrent tasks to wait on an operation. Pending futures are pushed to the list. When an operation completes, futures are popped from the list and resolved.
FutureList stores its futures as a linked list of FutureCores in its BaseFutureList base. This maximizes code reuse between different future implementations.
A FutureList is declared with a pointer to the
future implementation’s FutureCore member:
FutureList<&FutureType::future_core_>. For example:
1class MyFuture {
2 public:
3 // Future API: value_type, Pend(), is_completed()
4
5 private:
6 friend class MyFutureProvider;
7
8 void Resolve(int) { /* ... */ }
9
10 // The FutureCore is a member of the future.
11 pw::async2::FutureCore core_;
12};
13
14class MyFutureProvider {
15 public:
16 MyFuture Get() {
17 MyFuture future;
18 std::lock_guard lock(lock_);
19 futures_.Push(future);
20 return future;
21 }
22
23 void ResolveOne() {
24 std::lock_guard lock(lock_);
25 // Pop the future from the list as a MyFuture&.
26 futures_.Pop().Resolve(123);
27 }
28
29 private:
30 pw::sync::InterruptSpinLock lock_;
31
32 // FutureList is declared with a pointer to the future type's FutureCore.
33 pw::async2::FutureList<&MyFuture::core_> futures_ PW_GUARDED_BY(lock_);
34};
Waking mechanism#
When a task polls a future and it returns Pending, the future must store the
task’s Waker from the provided Context. This is handled automatically by
FutureCore::DoPend.
On the other side of the asynchronous operation (e.g., in an interrupt handler),
when the operation completes, the provider is used to retrieve the future, and
its Wake() function is called. This notifies the dispatcher that the task
waiting on this future is ready to make progress and should be polled again.
Setting up wakers#
Futures typically store a waker. When the future is ready to advance, that wake the task that pended them with this waker. Wakers can be set using one of these four macros:
PW_ASYNC_STORE_WAKER and PW_ASYNC_CLONE_WAKER
The first creates a waker for a given context. The second clones an existing waker, allowing the original and/or the clone to wake the task.
This pair of macros ensure a single task will be woken. They will assert if a waker for a different task is created (or cloned) when the destination waker already is set up for some task.
PW_ASYNC_TRY_STORE_WAKER and PW_ASYNC_TRY_CLONE_WAKER
These are alternatives to PW_ASYNC_STORE_WAKER and cc:PW_ASYNC_CLONE_WAKER that return
falseinstead of crashing if the waker is already set. This allows the caller to handle cases when the waker is already in use.
Example: Waiting for a GPIO interrupt#
Below is an example of a custom future that waits for a GPIO button press using
interfaces from pw_digital_io.
1class ButtonReceiver;
2
3class ButtonFuture {
4 public:
5 // All futures must define `value_type` as the return type from `Pend()`.
6 using value_type = void;
7
8 // Futures must be default constructible.
9 constexpr ButtonFuture() = default;
10
11 // FutureCore is movable and handles list management automatically.
12 ButtonFuture(ButtonFuture&&) = default;
13 ButtonFuture& operator=(ButtonFuture&&) = default;
14
15 // Polls the future to see if the button has been pressed.
16 pw::async2::Poll<> Pend(pw::async2::Context& cx) {
17 return core_.DoPend(*this, cx);
18 }
19
20 bool is_pendable() const { return core_.is_pendable(); }
21 bool is_complete() const { return core_.is_complete(); }
22
23 private:
24 friend class ButtonReceiver;
25 friend class pw::async2::FutureCore;
26
27 // Provide a descriptive reason which can be used to debug blocked tasks.
28 static constexpr const char kWaitReason[] = "Waiting for button press";
29
30 // Private constructor used by ButtonReceiver.
31 explicit ButtonFuture(pw::async2::FutureState::Pending)
32 : core_(pw::async2::FutureState::kPending) {}
33
34 // Callback invoked by FutureCore::DoPend.
35 pw::async2::Poll<> DoPend(pw::async2::Context&) {
36 if (core_.is_ready()) {
37 return pw::async2::Ready();
38 }
39 return pw::async2::Pending();
40 }
41
42 pw::async2::FutureCore core_;
43};
44
45static_assert(pw::async2::Future<ButtonFuture>);
46
47class ButtonReceiver {
48 public:
49 explicit ButtonReceiver(pw::digital_io::DigitalInterrupt& line)
50 : line_(line) {
51 PW_CHECK_OK(line_.SetInterruptHandler(
52 pw::digital_io::InterruptTrigger::kActivatingEdge,
53 [this](pw::digital_io::State) { HandleInterrupt(); }));
54 PW_CHECK_OK(line_.EnableInterruptHandler());
55 }
56
57 // Returns a future that completes when the button is pressed.
58 ButtonFuture WaitForPress() {
59 std::lock_guard lock(lock_);
60 ButtonFuture future(pw::async2::FutureState::kPending);
61 // Only allow one waiter at a time.
62 list_.PushRequireEmpty(future);
63 return future;
64 }
65
66 private:
67 // Executed in interrupt context.
68 void HandleInterrupt() {
69 std::lock_guard lock(lock_);
70 list_.ResolveAll();
71 }
72
73 pw::digital_io::DigitalInterrupt& line_;
74 pw::sync::InterruptSpinLock lock_;
75 pw::async2::FutureList<&ButtonFuture::core_> list_ PW_GUARDED_BY(lock_);
76};
This example demonstrates the core mechanics of creating a custom future. This
pattern of waiting for a single value from a producer is so common that
pw_async2 provides ValueFuture, which is
produced by a ValueProvider or
OptionalValueProvider, to handle it.
In practice, you would return a VoidFuture (alias
for ValueFuture<void>) from WaitForPress instead of writing a custom
ButtonFuture.
Implementing a composite future#
While leaf futures manage wakers and handle direct interaction with hardware or external providers, non-leaf functions in an execution graph often need to combine multiple asynchronous operations into higher-level business logic.
A composite future exists in the middle of an async execution graph:
Top level: Task implementations posted directly to the Dispatcher. Heavier weight as they hold lists and other dispatcher metadata.
Middle level: Composite futures and async helper functions that combine several asynchronous steps into a single logical unit.
Leaf level: Wakeable futures (such as TimeFuture or ValueFuture) that asynchronously wait on external signals, like hardware interrupts, network operations, or timers.
Unlike leaf futures, a composite future does not use FutureCore. It has no wakers and does not exist in an intrusive list or provider. Instead, it is owned entirely by its caller as a value object on the stack, with no external backreferences. Waker registration is handled transitively by the child futures stored inline inside the composite future.
Async helper function pattern#
When defining an async helper function that returns a composite future, follow these conventions:
Use the factory pattern. The function acts as a factory constructing composite futures and returning them directly by value. There is no provider, no list or waker management. Those occur within the subfutures that perform wakeable operations.
Return Future objects directly. Per
pw_async2conventions, the function must return a future directly instead of wrapping it in aResultorstd::optional. This enables further composability, including allowing callers toco_awaitthe function.Handle errors through resolved futures. The function can run synchronous validation before triggering the first async operation, returning a future that immediately resolves to an error if invalid.
Example: Retry logic with composite futures#
Below is an example demonstrating a composite future implementation,
ReadSensorWithRetryFuture with its factory helper function
ReadSensorWithRetry. It combines a sensor read (via ValueFuture) and a delay (via TimeFuture) into a single state machine without
any dynamic allocation, provider registration, or task overhead.
1/// A composite future that reads a sensor with retry on failure.
2///
3/// This future exists in the middle of an async execution graph: the top level
4/// contains `Task` implementations posted directly to the `Dispatcher`, while
5/// the leaves are futures that asynchronously wait on external signals, like
6/// hardware interrupts or timers. This future sits between those, combining
7/// several other asynchronous operations into a logical unit.
8///
9/// Unlike leaf futures, this does not use `FutureCore`. It has no wakers, and
10/// does not exist in a linked list. It is owned entirely by its caller, with
11/// nothing else in the system maintaining any references to it. These types
12/// of composite futures allow bundling and encapsulating multi-step async
13/// logic in a composable and reusable way.
14class ReadSensorWithRetryFuture {
15 public:
16 // Future concept requirement: define the result value type.
17 using value_type = pw::Result<int>;
18
19 // Futures must be default constructible and movable.
20 ReadSensorWithRetryFuture() = default;
21 ReadSensorWithRetryFuture(ReadSensorWithRetryFuture&&) = default;
22 ReadSensorWithRetryFuture& operator=(ReadSensorWithRetryFuture&&) = default;
23
24 // Future concept requirement: check if the operation can be pended.
25 bool is_pendable() const {
26 return state_ != State::kUninitialized && state_ != State::kDone;
27 }
28
29 // Future concept requirement: check if the operation has completed.
30 bool is_complete() const { return state_ == State::kDone; }
31
32 // Drives the composite state machine forward.
33 Poll<pw::Result<int>> Pend(Context& cx) {
34 while (true) {
35 switch (state_) {
36 case State::kUninitialized:
37 PW_CRASH("Polled an uninitialized ReadSensorWithRetryFuture");
38
39 case State::kInitializing: {
40 if (!immediate_error_.IsUnknown()) {
41 state_ = State::kDone;
42 return pw::async2::Ready(immediate_error_);
43 }
44
45 read_future_ = sensor_->Read();
46 state_ = State::kReading;
47 break;
48 }
49
50 case State::kReading: {
51 // Pend the child sensor future, passing `cx` down.
52 // The leaf future will handle registering wakers if it returns
53 // `Pending`.
54 PW_AWAIT(pw::Result<int> res, read_future_, cx);
55
56 // If the read succeeded or we have no retries left, complete the
57 // future.
58 if (res.ok() || retries_left_ == 0) {
59 state_ = State::kDone;
60 return pw::async2::Ready(res);
61 }
62
63 // Read failed: prepare for retry timer.
64 retries_left_--;
65 timer_future_ =
66 time_provider_->WaitFor(std::chrono::milliseconds(50));
67 state_ = State::kWaitingToRetry;
68 break; // Loop immediately to ensure the timer is pended.
69 }
70
71 case State::kWaitingToRetry: {
72 // Pend the child time future.
73 Poll<SystemClock::time_point> timer_res = timer_future_.Pend(cx);
74 if (timer_res.IsPending()) {
75 return pw::async2::Pending();
76 }
77
78 // Delay finished. Start a new sensor read and loop back to kReading.
79 read_future_ = sensor_->Read();
80 state_ = State::kReading;
81 break; // Loop immediately to pend the new sensor read.
82 }
83
84 case State::kDone:
85 PW_CRASH("Polled a completed ReadSensorWithRetryFuture");
86 }
87 }
88 }
89
90 private:
91 friend ReadSensorWithRetryFuture ReadSensorWithRetry(
92 MockSensor& sensor,
93 SimulatedTimeProvider<SystemClock>& time_provider,
94 int max_retries);
95
96 ReadSensorWithRetryFuture(MockSensor& sensor,
97 SimulatedTimeProvider<SystemClock>& time_provider,
98 int max_retries)
99 : state_(State::kInitializing),
100 sensor_(&sensor),
101 time_provider_(&time_provider),
102 retries_left_(max_retries) {}
103
104 // Constructs a future that immediately fails with the specified status.
105 explicit ReadSensorWithRetryFuture(pw::Status status)
106 : state_(State::kInitializing), immediate_error_(status) {
107 PW_ASSERT(!status.ok() && !status.IsUnknown());
108 }
109
110 enum class State {
111 kUninitialized,
112 kInitializing,
113 kReading,
114 kWaitingToRetry,
115 kDone
116 };
117 State state_ = State::kUninitialized;
118
119 MockSensor* sensor_ = nullptr;
120 SimulatedTimeProvider<SystemClock>* time_provider_ = nullptr;
121 int retries_left_ = 0;
122 pw::Status immediate_error_ = pw::Status::Unknown();
123
124 // Owns the child futures inline.
125 ValueFuture<pw::Result<int>> read_future_;
126 TimeFuture<SystemClock> timer_future_;
127};
128
129// Verify that ReadSensorWithRetryFuture satisfies the Future concept.
130static_assert(pw::async2::Future<ReadSensorWithRetryFuture>);
131
132/// An async helper function.
133///
134/// The function is a factory constructing composite futures and returning them
135/// directly by value. There is no provider, no list or waker management. Those
136/// occur within the subfutures that actually perform wakeable operations.
137///
138/// The function begins by synchronously validating its arguments, returning a
139/// future that immediately resolves to an error if invalid.
140///
141/// Per async2 conventions, the function returns a future directly instead of
142/// wrapping it in a `Result` / `std::optional` to allow further composition,
143/// or, in the coroutine world:
144///
145/// @code{.cpp}
146/// pw::Result<int> result =
147/// co_await ReadSensorWithRetry(sensor,
148/// GetSystemTimeProvider(),
149/// 10);
150/// @endcode
151inline ReadSensorWithRetryFuture ReadSensorWithRetry(
152 MockSensor& sensor,
153 SimulatedTimeProvider<SystemClock>& time_provider,
154 int max_retries = 3) {
155 if (max_retries <= 0) {
156 return ReadSensorWithRetryFuture(pw::Status::InvalidArgument());
157 }
158 return ReadSensorWithRetryFuture(sensor, time_provider, max_retries);
159}
Derived value futures#
Sometimes a provider needs to inspect the specific constraints of a request before deciding to fulfill it (e.g., an allocator checking if the requested size is available). With a standard ValueProvider, the provider only knows that a request exists, but cannot attach additional information to it or safely inspect that information.
To support this, Pigweed allows you to derive from ValueFuture<T> to add custom fields, and use DerivedValueProvider<DerivedFuture> to manage them.
Creating a derived future#
To create a derived future, inherit from ValueFuture<T> and provide a constructor that accepts the base
future by move (ValueFuture<T>&&) along with any custom arguments.
class BufferFuture : public pw::async2::ValueFuture<pw::Result<pw::ByteSpan>> {
public:
BufferFuture(pw::async2::ValueFuture<pw::Result<pw::ByteSpan>>&& base,
size_t requested_size)
: pw::async2::ValueFuture<pw::Result<pw::ByteSpan>>(std::move(base)),
requested_size_(requested_size) {}
size_t requested_size() const { return requested_size_; }
private:
size_t requested_size_;
};
Atomic inspection and resolution with ResolveIf#
The core feature of DerivedValueProvider is the ResolveIf method.
It allows the provider to inspect the pending future and conditionally resolve
it atomically, preventing race conditions where a future might be cancelled
between inspection and resolution.
ResolveIf takes a callback function that receives a reference to your
derived future type. The behavior depends on whether the future produces a value:
Value-returning futures: The callback returns a
std::optional<T>. If it returns a value, the future is popped and resolved with that value. If it returnsstd::nullopt, the future remains pending in the list.Void futures: The callback returns a
bool. If it returnstrue, the future is popped and resolved.
pw::async2::DerivedValueProvider<BufferFuture> provider;
// Attempt to resolve the request.
bool resolved = provider.ResolveIf(
[](BufferFuture& future) -> std::optional<pw::Result<pw::ByteSpan>> {
// Inspect the request parameters to decide if it can be fulfilled.
if (future.requested_size() <= available_memory) {
return Allocate(future.requested_size());
}
return std::nullopt;
});
Warning
Since ResolveIf holds a shared async2 lock while executing the callback,
the callback code should be fast and non-blocking. Avoid slow operations
inside the callback if possible as they could stall running tasks.
Multi-consumer list providers#
A standard ValueProvider only allows a single pending future at a time. To manage multiple pending futures, you can use ValueListProvider.
With ValueListProvider, any number of tasks can register futures in a list. The provider owner can then inspect, conditionally resolve, or bulk-abort pending futures from anywhere in the list.
This is particularly useful for implementing resource reservation systems (e.g., memory allocators, connection pools) where out-of-order resolution is necessary to completely prevent head-of-line blocking.
Creating a list provider#
Declare a ValueListProvider with the type of value to provide:
pw::async2::ValueListProvider<int> provider;
Tasks can register futures using the Get method, which returns a
ValueFuture and automatically pushes it to
the provider’s internal list:
pw::async2::ValueFuture<int> future = provider.Get();
Querying list state#
You can safely query the number of pending futures using size() or check
if the list is empty using empty().
Atomic out-of-order matching#
To prevent time-of-check to time-of-use (TOCTOU) race conditions in
multithreaded environments, ValueListProvider does not expose list
iteration. Instead, all matching and resolution must be performed atomically
using ResolveFirstMatching and ResolveAllMatching.
These methods traverse the list under a shared async2 lock and invoke a
callback for each pending future. If the callback returns a value (or true
for void futures), the matched future is atomically removed from the list
and resolved.
ResolveFirstMatching: Resolves the first future for which the callback returns a value.ResolveAllMatching: Resolves all futures for which the callback returns a value.
For non-void futures (producing T), the callback must return
std::optional<T>. Returning std::nullopt leaves the future pending.
For void futures, the callback must return bool.
See the section below for a concrete example of using these matching methods with custom derived futures to achieve out-of-order resource allocation.
Using custom derived futures#
Just like DerivedValueProvider,
ValueListProvider can be used with user-defined derived futures. You can
use the DerivedValueListProvider template alias to simplify
declarations:
class CustomRequestFuture
: public pw::async2::ValueFuture<pw::Result<pw::ByteSpan>> {
public:
CustomRequestFuture(pw::async2::ValueFuture<pw::Result<pw::ByteSpan>>&& base,
size_t requested_size)
: pw::async2::ValueFuture(std::move(base)),
requested_size_(requested_size) {}
size_t requested_size() const { return requested_size_; }
private:
size_t requested_size_;
};
// Declare a list provider for the derived future type.
pw::async2::DerivedValueListProvider<CustomRequestFuture> request_provider;
Now, you can pass custom parameters when calling Get:
CustomRequestFuture future = request_provider.Get(/*requested_size=*/128);
You can then atomically match and resolve using the custom parameters on the derived future:
bool resolved = request_provider.ResolveFirstMatching(
[&](CustomRequestFuture& future)
-> std::optional<pw::Result<pw::ByteSpan>> {
if (future.requested_size() <= GetAvailableBytes()) {
return Allocate(future.requested_size());
}
return std::nullopt;
});
Bulk resolution and cleanup#
When a resource is shut down or connection is lost, you may want to resolve
all remaining futures at once. Use the ResolveAll method to resolve and
remove all pending futures from the list.
The callback is invoked for every pending future:
For non-void futures, the callback must return the value (of type
T) to resolve the future with.For void futures, the callback acts as a notification.
// Abort all pending requests with an error status.
request_provider.ResolveAll(
[](CustomRequestFuture& future) -> pw::Result<pw::ByteSpan> {
return pw::Status::Aborted();
});
Warning
Since all callback-based resolution methods hold the shared async2 lock, your callbacks must be fast and non-blocking.
Combinators#
Combinators allow you to compose multiple futures into a single future to express complex control flow.
Join#
Join() waits for multiple futures to complete and returns a tuple of their results.
ValueFuture<pw::Status> DoWork(int id);
class JoinTask : public pw::async2::Task {
private:
pw::async2::Poll<> DoPend(pw::async2::Context& cx) override {
if (!future_.is_pendable()) {
// Start three futures concurrently and wait for all of them
// to complete.
future_ = pw::async2::Join(DoWork(1), DoWork(2), DoWork(3));
}
PW_AWAIT(auto results, future_, cx);
auto [status1, status2, status3] = std::move(results);
if (!status1.ok() || !status2.ok() || !status3.ok()) {
PW_LOG_ERROR("Operation failed");
} else {
PW_LOG_INFO("All operations succeeded");
}
return pw::async2::Ready();
}
JoinFuture<ValueFuture<pw::Status>,
ValueFuture<pw::Status>,
ValueFuture<pw::Status>>
future_;
};
pw::async2::Coro<pw::Status> JoinExample(pw::async2::CoroContext) {
// Start three futures concurrently and wait for all of them to complete.
auto [status1, status2, status3] =
co_await pw::async2::Join(DoWork(1), DoWork(2), DoWork(3));
if (!status1.ok() || !status2.ok() || !status3.ok()) {
PW_LOG_ERROR("Operation failed");
co_return pw::Status::Internal();
}
PW_LOG_INFO("All operations succeeded");
co_return pw::OkStatus();
}
Select#
Select() waits for the first of multiple futures to complete. It returns a SelectFuture which resolves to an OptionalTuple containing the result. If additional futures happen to complete between the first future completing the task re-running, the tuple stores all of their results.
#include "pw_async2/select.h"
ValueFuture<int> DoWork();
ValueFuture<int> DoOtherWork();
class SelectTask : public pw::async2::Task {
private:
pw::async2::Poll<> DoPend(pw::async2::Context& cx) override {
if (!future_.is_pendable()) {
// Race two futures and wait for the first one to complete.
future_ = pw::async2::Select(DoWork(), DoOtherWork());
}
PW_AWAIT(auto results, future_, cx);
// Check which future(s) completed.
// In this example, we check all of them, but it's common to return
// after the first result.
if (results.has_value<0>()) {
PW_LOG_INFO("DoWork completed with: %d", results.value<0>());
}
if (results.has_value<1>()) {
PW_LOG_INFO("DoOtherWork completed with: %d", results.value<1>());
}
return pw::async2::Ready();
}
pw::async2::SelectFuture<ValueFuture<int>, ValueFuture<int>> future_;
};
pw::async2::Coro<void> SelectExample(pw::async2::CoroContext) {
// Race two futures and wait for the first one to complete.
auto results = co_await pw::async2::Select(DoWork(), DoOtherWork());
// Check which future(s) completed.
// In this example, we check all of them, but it's common to return
// after the first result.
if (results.has_value<0>()) {
int result = results.value<0>();
PW_LOG_INFO("DoWork completed with: %d", result);
}
if (results.has_value<1>()) {
int result = results.value<1>();
PW_LOG_INFO("DoOtherWork completed with: %d", result);
}
}
Type erasure with BoxedFuture#
Because pw_async2 futures heavily use C++ templates, and Future combinators
create complex nested future types, it can become difficult to name the return
type of an async function to store it in a task.
pw_async2 provides BoxedFuture for type
erasure when working with futures that return some value type T.
Some scenarios where a BoxedFuture can be useful include:
Complex combinators. If you use future combinators, the resulting type is often complex and difficult or impossible to spell out (especially if it relies on a local lambda).
Returning different future types from a single function. If you have conditional logic that performs different operations with underlying future implementations, you can use
BoxedFutureto unify them.Writing virtual interfaces. If you are defining an abstract base class with asynchronous operations,
BoxedFutureallows implementers to choose their own future types to return.
Note
pw_async2 is designed to be allocation-free by default. However,
BoxedFuture requires dynamic memory allocation via pw::Allocator.
Timing-out Futures#
If you create a future, you can also combine it with a TimeFuture to get a new composite future (a FutureWithTimeout) that can time out.
There are three main factory functions that construct useful variants of the composite type.
Timeout(future, [time_provider,] delay)This function returns a composite future that times out after the specified delay. If no time provider is given, the function will default to GetSystemTimeProvider. The time provider will then be used to construct the TimeFuture to use in the composite future.
If the original value future is for a value of type T, the created composite future uses pw::Result<T>. On timeout, the status associated with that result will be
Status.DeadlineExceeded()to make it clear no value is available.The composite future will handle waiting on both futures, and will prefer to resolve to the value provided by the first future if both futures are ready when they are next pended.
TimeoutOr(future, [time_provider,] delay, sentinel_value_or_func)Like the first function, this function will construct a composite future that will time out after the specified delay.
However on timeout, this version will resolve to a sentinel value, either using a value passed in, or calling a function to obtain it, if that is what is passed in.
Note that a copy of the value that is passed will be stored as part of the internal data for a future. For a small and trivially constructible type, this makes sense, but for a large type or a type that is not trivially constructible you should prefer to pass a function which constructs the value.
Caution
You should only use a sentinel when it there is no chance of confusing the sentinel value with the normal values you would obtain from the future when it does not time out.
TimeoutOrClosed(channel_future, [time_provider], delay)Like the first function, but intended to be used with the SendFuture, ReceiveFuture, and ReserveSendFuture futures returned from using a channel.
On timeout, these act like the channel was closed while waiting, and release their reference to the channel. For
SendFuture, this means it resolves to false, and for the other two it means resolving tostd::nullopt.
Example#
Using them to construct the composite future is easy.
// Obtain a basic ValueFuture<T> or similar from some provider.
auto value_future = value_provider.Get();
// Construct the composite future, which will either resolve
// to a `T`, or timeout after 15ms using the system clock.
auto future_with_timeout_ex1 = Timeout(std::move(value_future), 15ms);
// You can also construct one this way.
auto future_with_timeout_ex2 = Timeout(value_provider.Get(), 15ms);
// For a sentinel with a simple constant:
auto future_with_timeout_ex3 = TimeoutOr(int_value_provider.Get(), 15ms, -1);
// To use a function to obtain a sentinnel value:
auto future_with_timeout_ex4 =
TimeoutOr(int_value_provider.Get(), 15ms, []() { return -1; });
You can find more examples showing how to use these functions in pw_async2/examples/timeout_test.cc.