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:
CoroOrElseTask: Delegates to a provided coroutine and executes an
or_elsehandler function on failure.FutureCallbackTask: Invokes a callback after a future is ready. See Callbacks.
OwnedTask: Gives ownership to the dispatcher when the task is posted. The task must implement DoDestroy(), which the dispatcher invokes after the task completes.
PendFuncTask: Delegates to a provided function.
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
Taskobject is not destroyed while it is still registered with aDispatcher. Calling Task::Deregister() before destruction guarantees safety.Dynamic Allocation: For tasks with a dynamic lifetime,
pw_async2provides 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.