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; void if the future produces no value. The FutureValue<Future> helper resolves to the future’s value_type, but maps void to ReadyType so that a FutureValue<Future> can always be instantiated and referenced.

  • Poll<value_type> Pend(Context& cx): Calling Pend advances 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 when Pend should 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 Pending return 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 Try to 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, FutureOrValue holds the pending future.

  • When the future resolves to Ready, FutureOrValue immediately destroys the future (releasing any resources it held) and stores the resulting value in its place.

  • Calling Advance(cx) advances a pending future, returning true if the value is available or false if still pending. If the value has already been resolved, Advance(cx) returns true immediately 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 FutureOrValue is not a future; it is storage. APIs must always return futures directly.

  • Do not pass ``FutureOrValue`` around. FutureOrValue should 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 FutureOrValue when 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:

  1. Empty: The default state, or after the value has been extracted via Take() or cleared via Reset(). No future is active and no value is stored. empty() returns true.

  2. Pending: A future has been assigned but has not yet resolved. has_future() returns true.

  3. Ready: The future has resolved and the value is stored. has_value() returns true.

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 false instead 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_async2 conventions, the function must return a future directly instead of wrapping it in a Result or std::optional. This enables further composability, including allowing callers to co_await the 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 returns std::nullopt, the future remains pending in the list.

  • Void futures: The callback returns a bool. If it returns true, 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:

  1. 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).

  2. Returning different future types from a single function. If you have conditional logic that performs different operations with underlying future implementations, you can use BoxedFuture to unify them.

  3. Writing virtual interfaces. If you are defining an abstract base class with asynchronous operations, BoxedFuture allows 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 to std::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.