Tasks#

pw_async2: Cooperative async tasks for embedded

Tasks are the top-level “thread” primitives of pw_async2. This guide provides detailed usage information about the Task API of pw_async2.

Background#

For a detailed conceptual explanation of tasks, see Informed poll. For a hands-on introduction to using tasks, see Codelab.

Concrete subclasses#

Pigweed provides the following concrete subclasses of Task:

You can also create your own subclass.

Callbacks#

In a system gradually or partially adopting pw_async2, there are often cases where existing code needs to run asynchronous operations built with pw_async2. To facilitate this, pw_async2 provides FutureCallbackTask. This task invokes a future, forwarding its result to a provided callback on completion.

Example#

#include "pw_async2/callback_task.h"
#include "pw_log/log.h"
#include "pw_result/result.h"

// Assume the async2 part of the system exposes a function to post tasks to
// its dispatcher.
void PostTaskToDispatcher(pw::async2::Task& task);

// The async2 function we'd like to call.
pw::async2::Future<pw::Result<int>> ReadValue();

// Non-async2 code.
void ReadAndPrintAsyncValue() {
  pw::async2::FutureCallbackTask task(ReadValue(), [](pw::Result<int> result) {
    if (result.ok()) {
      PW_LOG_INFO("Read value: %d", result.value());
    } else {
      PW_LOG_ERROR("Failed to read value: %s", result.status().str());
    }
  });

  PostTaskToDispatcher(task);

  // In this example, the code allocates the task on the stack, so we would
  // need to wait for it to complete before it goes out of scope. In a real
  // application, the task may be a member of a long-lived object, or you
  // might choose to statically allocate it.
}

Considerations for callback-based integration#

While the FutureCallbackTask helper is convenient, each instance of it is a distinct Task in the system which will compete for execution with other tasks running on the dispatcher.

If an asynchronous part of the system needs to expose a robust, primary API based on callbacks to non-pw_async2 code, a more integrated solution is recommended. Instead of using standalone FutureCallbackTask objects, the Task that manages the operation should natively support registering and managing a list of callbacks. This provides a clearer and more efficient interface for external consumers.

Memory#

The memory for a Task object itself is managed by the user. This provides flexibility in how tasks are allocated and stored. Common patterns include:

  • Static or Member Storage: For tasks that live for the duration of the application or are part of a long-lived object, they can be allocated statically or as class members. This is the most common and memory-safe approach. The user must ensure the Task object is not destroyed while it is still registered with a Dispatcher. Calling Task::Deregister() before destruction guarantees safety.

  • Dynamic Allocation: For tasks with a dynamic lifetime, pw_async2 provides the AllocateTask() helper. See Dynamically allocating tasks.

Dynamically allocating tasks#

AllocateTask() creates a concrete subclass of Task, just like PendableAsTask, but the created task is dynamically allocated using a provided pw::Allocator. Upon completion the associated memory is automatically freed by calling the allocator’s Delete method. This simplifies memory management for “fire-and-forget” tasks.

// This task will be deallocated from the provided allocator when it's done.
Task* task = AllocateTask<MyPendable>(my_allocator, arg1, arg2);
dispatcher.Post(*task);

Implementing tasks#

The following sections provide more guidance about subclassing Task yourself.

Pend() and DoPend(), the core interfaces#

A dispatcher drives a task forward by invoking the task’s Pend() method. Pend() is a non-virtual wrapper around the task’s DoPend() method. DoPend() is where the core logic of a task should be implemented.

Communicating completion state#

When a task is incomplete but can’t make any more progress, its DoPend() method should return Pending. The dispatcher will sleep the task.

When a task has completed all work, it should return Ready.

Note

How does a task wake back up? Tasks are not directly involved in this process. The task invokes one or more asynchronous operations that return futures, which are values that may not be complete yet. When invoking the async operation, the task provides its context. The future grabs a waker from the context. When the future’s value is ready, the asynchronous operation invokes the waker to inform the dispatcher that the task can make more progress. See Futures: The basic async primitive and Wakers: Progress updates for the dispatcher for further explanation.

Cleaning up complete tasks#

The behavior of a task after returning Ready is implementation-specific. For a one-shot operation, it may be an error to poll it again. For a stream-like operation (e.g. reading from a channel), polling again after a Ready result is the way to receive the next value. This behavior should be clearly documented.

Passing data between tasks#

See Channels.

Debugging#

You can inspect tasks registered to a dispatcher by calling :Dispatcher::LogRegisteredTasks(), which logs information for each task in the dispatcher’s pending and sleeping queues.

Sleeping tasks will log information about their assigned wakers, with the wait reason provided for each.

If space is a concern, you can set the module configuration option PW_ASYNC2_DEBUG_WAIT_REASON to 0 to disable wait reason storage and logging. Under this configuration, the dispatcher only logs the waker count of a sleeping task.