Task APIs
Task
The Task component provides a cross-platform API around std::thread with some additional configuration of stack size, priority, and core affinity for FreeRTOS / ESP, as well as providing a callback API which enables interruptible sleeps and termination of the task.
Code examples for the task API are provided in the task example folder.
API Reference
Header File
Classes
-
class Task : public espp::BaseComponent
Task provides an abstraction over std::thread which optionally includes memory / priority configuration on ESP systems. It allows users to easily stop the task, and will automatically stop itself if destroyed.
There is also a utility function which can be used to get the info for the task of the current context, or for a provided Task object.
There is also a helper function to run a lambda on a specific core, which can be used to run a specific function on a specific core, as you might want to do when registering an interrupt driver on a specific core.
Basic Task Example
espp::Task::configure_task_watchdog(1000ms); auto task_fn = [](std::mutex &m, std::condition_variable &cv) { static size_t task_iterations{0}; fmt::print("Task: #iterations = {}\n", task_iterations); task_iterations++; // NOTE: sleeping in this way allows the sleep to exit early when the // task is being stopped / destroyed { std::unique_lock<std::mutex> lk(m); cv.wait_for(lk, 500ms); } // we don't want to stop, so return false return false; }; auto task = espp::Task({.callback = task_fn, .task_config = {.name = "Task 1"}, .log_level = espp::Logger::Verbosity::DEBUG}); task.start(); task.start_watchdog(); // start the watchdog timer for this task std::this_thread::sleep_for(num_seconds_to_run * 1s); task.stop_watchdog(); // stop the watchdog timer for this task // show explicitly stopping the task (though the destructor called at the // end of this scope would do it for us) task.stop(); std::error_code ec; std::string watchdog_info = espp::Task::get_watchdog_info(ec); if (ec) { fmt::print("Error getting watchdog info: {}\n", ec.message()); } else if (!watchdog_info.empty()) { fmt::print("Watchdog info: {}\n", watchdog_info); } else { fmt::print("No watchdog info available\n"); }
Task Watchdog Example
static constexpr bool panic_on_watchdog_timeout = false; espp::Task::configure_task_watchdog(300ms, panic_on_watchdog_timeout); auto task_fn = [](std::mutex &m, std::condition_variable &cv) { static size_t task_iterations{0}; fmt::print("Task: #iterations = {}\n", task_iterations); task_iterations++; std::unique_lock<std::mutex> lk(m); // note our sleep here is longer than the watchdog timeout, so we should // trigger the watchdog timeout cv.wait_for(lk, 500ms); // we don't want to stop, so return false return false; }; auto task = espp::Task({.callback = task_fn, .task_config = {.name = "Task 1"}, .log_level = espp::Logger::Verbosity::DEBUG}); task.start(); task.start_watchdog(); // start the watchdog timer for this task std::this_thread::sleep_for(500ms); std::error_code ec; std::string watchdog_info = espp::Task::get_watchdog_info(ec); if (ec) { fmt::print("Error getting watchdog info: {}\n", ec.message()); } else if (!watchdog_info.empty()) { fmt::print("Watchdog info: {}\n", watchdog_info); } else { fmt::print("No watchdog info available\n"); } // NOTE: the task and the watchdog will both automatically get stopped when // the task goes out of scope and is destroyed.
Many Task Example
std::vector<std::unique_ptr<espp::Task>> tasks; size_t num_tasks = 10; logger.info("Many task example: spawning {} tasks!", num_tasks); tasks.resize(num_tasks); auto start = std::chrono::high_resolution_clock::now(); for (size_t i = 0; i < num_tasks; i++) { size_t iterations{0}; // copy the loop variables and indicate that we intend to mutate them! auto task_fn = [i, iterations](std::mutex &m, std::condition_variable &cv) mutable { fmt::print("Task {}: #iterations = {}\n", i, iterations); iterations++; // NOTE: sleeping in this way allows the sleep to exit early when the // task is being stopped / destroyed { std::unique_lock<std::mutex> lk(m); cv.wait_for(lk, 1s); } // we don't want to stop, so return false return false; }; std::string task_name = fmt::format("Task {}", i); auto task = espp::Task::make_unique({.callback = task_fn, .task_config = {.name = task_name}}); tasks[i] = std::move(task); tasks[i]->start(); } fmt::print("Tasks spawned, waiting for {} seconds!\n", num_seconds_to_run); std::this_thread::sleep_until(start + num_seconds_to_run * 1s);
Long Running Task Example
auto task_fn = [](std::mutex &m, std::condition_variable &cv) { static size_t task_iterations{0}; const size_t num_steps_per_iteration = 10; fmt::print("Task processing iteration {}...\n", task_iterations); for (size_t i = 0; i < num_steps_per_iteration; i++) { // NOTE: sleeping in this way allows the sleep to exit early when the // task is being stopped / destroyed { std::unique_lock<std::mutex> lk(m); // NOTE: using the return value from the cv.wait_for() allows us to // know if the task was asked to stop, for which we can handle and // return early. auto cv_retval = cv.wait_for(lk, std::chrono::milliseconds(100)); if (cv_retval == std::cv_status::no_timeout) { // if there was no timeout, then we were notified, therefore we need // to shut down. fmt::print("Task stopping early (step {}/{}) on iteration {}\n", i, num_steps_per_iteration, task_iterations); // NOTE: use this_thread::sleep_for() to fake cleaning up work that // we would do std::this_thread::sleep_for(10ms); // now that we've (fake) cleaned-up our work, return from the task // function so the task can fully destruct. return false; } } } fmt::print("Task processing iteration {} complete\n", task_iterations); task_iterations++; // we don't want to stop, so return false return false; }; auto task = espp::Task({.callback = task_fn, .task_config = {.name = "Complex Task"}, .log_level = espp::Logger::Verbosity::DEBUG}); task.start();
Long Running Task Notified Example (Recommended)
auto task_fn = [](std::mutex &m, std::condition_variable &cv, bool &task_notified) { static size_t task_iterations{0}; const size_t num_steps_per_iteration = 10; fmt::print("Task processing iteration {}...\n", task_iterations); for (size_t i = 0; i < num_steps_per_iteration; i++) { // NOTE: sleeping in this way allows the sleep to exit early when the // task is being stopped / destroyed { std::unique_lock<std::mutex> lk(m); // NOTE: using the return value from the cv.wait_for() allows us to // know if the task was asked to stop, for which we can handle and // return early. auto stop_requested = cv.wait_for(lk, std::chrono::milliseconds(100), [&task_notified] { return task_notified; }); if (stop_requested) { fmt::print("Task was notified, stopping early (step {}/{}) on iteration {}\n", i, num_steps_per_iteration, task_iterations); // NOTE: use this_thread::sleep_for() to fake cleaning up work that // we would do std::this_thread::sleep_for(10ms); // now that we've (fake) cleaned-up our work, return from the task // function so the task can fully destruct. return false; } } } fmt::print("Task processing iteration {} complete\n", task_iterations); task_iterations++; // we don't want to stop, so return false return false; }; auto task = espp::Task({.callback = task_fn, .task_config = {.name = "Notified Complex Task"}, .log_level = espp::Logger::Verbosity::DEBUG}); task.start();
Task Info Example
auto task_fn = [](std::mutex &m, std::condition_variable &cv) { static size_t task_iterations{0}; task_iterations++; // allocate stack size_t num_bytes = 256 * task_iterations; char buffer[num_bytes]; // do something with the bufer (which also uses stack) snprintf(buffer, num_bytes, "%.06f\n", (float)task_iterations); fmt::print("{}\n", espp::Task::get_info()); // NOTE: sleeping in this way allows the sleep to exit early when the // task is being stopped / destroyed { std::unique_lock<std::mutex> lk(m); cv.wait_for(lk, 500ms); } // we don't want to stop, so return false return false; }; auto task = espp::Task({.callback = task_fn, .task_config = {.name = "DynamicTask"}, .log_level = espp::Logger::Verbosity::DEBUG}); task.start(); auto now = std::chrono::high_resolution_clock::now(); auto elapsed = std::chrono::duration<float>(now - test_start).count(); while (elapsed < num_seconds_to_run) { std::this_thread::sleep_for(200ms); now = std::chrono::high_resolution_clock::now(); elapsed = std::chrono::duration<float>(now - test_start).count(); } fmt::print("Final: {}\n", espp::Task::get_info(task));
Task Request Stop Example
auto task_fn = [&num_seconds_to_run](std::mutex &m, std::condition_variable &cv) { static auto begin = std::chrono::high_resolution_clock::now(); auto now = std::chrono::high_resolution_clock::now(); auto elapsed = std::chrono::duration<float>(now - begin).count(); if (elapsed > num_seconds_to_run) { // we've gone long enough, time to stop our task! return true; } // NOTE: sleeping in this way allows the sleep to exit early when the // task is being stopped / destroyed { std::unique_lock<std::mutex> lk(m); cv.wait_for(lk, 10ms); } // we don't want to stop yet, so return false return false; }; auto task = espp::Task({.callback = task_fn, .task_config = {.name = "AutoStop Task"}, .log_level = espp::Logger::Verbosity::DEBUG}); task.start(); while (task.is_started()) { std::this_thread::sleep_for(10ms); }
Run on Core Example
logger.info("espp::task::run_on_core example: main thread core ID: {}", xPortGetCoreID()); // NOTE: in these examples, because we're logging with libfmt in the // function to be run, we need a little more than the default 2k stack size, // so we're using 3k. // test running a function that returns void on a specific core auto task_fn = []() -> void { fmt::print("Void Task running on core {}\n", xPortGetCoreID()); }; espp::task::run_on_core(task_fn, 0, 3 * 1024); fmt::print("Void Function returned\n"); espp::task::run_on_core(task_fn, 1, 3 * 1024); fmt::print("Void Function returned\n"); // test running a function that returns bool on a specific core auto task_fn2 = []() -> bool { auto core_id = xPortGetCoreID(); fmt::print("Bool Task running on core {}\n", core_id); return core_id == 1; }; auto result0 = espp::task::run_on_core(task_fn2, 0, 3 * 1024); fmt::print("Bool Function returned {}\n", result0); auto result1 = espp::task::run_on_core( task_fn2, {.name = "test", .stack_size_bytes = 3 * 1024, .core_id = 1}); fmt::print("Bool Function returned {}\n", result1); // test running a function that returns esp_err_t on a specific core auto task_fn3 = []() -> esp_err_t { auto core_id = xPortGetCoreID(); fmt::print("esp_err_t Task running on core {}\n", core_id); return core_id == 1 ? ESP_OK : ESP_FAIL; }; auto err0 = espp::task::run_on_core(task_fn3, 0, 3 * 1024); fmt::print("esp_err_t Function returned {}\n", esp_err_to_name(err0)); auto err1 = espp::task::run_on_core(task_fn3, 1, 3 * 1024); fmt::print("esp_err_t Function returned {}\n", esp_err_to_name(err1));
Run on Core (Non-Blocking) Example
logger.info("espp::task::run_on_core non-blocking example: main thread core ID: {}", xPortGetCoreID()); // NOTE: in these examples, because we're logging with libfmt in the // function to be run, we need a little more than the default 2k stack size, // so we're using 3k. // test running a function which takes a while to complete auto task_fn = []() -> void { fmt::print("[{0}] Task running on core {0}\n", xPortGetCoreID()); std::this_thread::sleep_for(1s); fmt::print("[{0}] Task done!\n", xPortGetCoreID()); }; espp::task::run_on_core_non_blocking(task_fn, 0, 3 * 1024); espp::task::run_on_core_non_blocking(task_fn, {.name = "test", .core_id = 1}); fmt::print("Started tasks on cores 0 and 1\n"); // sleep for a bit to let the tasks run std::this_thread::sleep_for(2s);
Public Types
-
typedef std::function<bool(std::mutex &m, std::condition_variable &cv, bool ¬ified)> callback_m_cv_notified_fn
Task callback function signature.
Note
The callback is run repeatedly within the Task, therefore it MUST return, and also SHOULD have a sleep to give the processor over to other tasks. For this reason, the callback is provided a std::condition_variable (and associated mutex) which the callback can use when they need to wait. If the cv.wait_for / cv.wait_until return false, no action is necessary (as the timeout properly occurred / the task was not notified). If they return true, then the callback function should return immediately since the task is being stopped (optionally performing any task-specific tear-down).
- Param m
mutex associated with the condition variable (should be locked before calling cv.wait_for() or cv.wait_until())
- Param cv
condition variable the callback can use to perform an interruptible wait. Use the notified parameter as the predicate for the wait to ensure spurious wake-ups are properly ignored.
- Param notified
Whether or not the task has been notified to stop. This is a reference to the task’s notified member, and is intended to be used with the condition variable parameter cv when waiting to avoid spurious wakeups. The task function is responsible for clearing this flag after each wait. Do not release the lock on the mutex m until the notified flag has been checked and cleared.
- Return
Whether or not the callback’s thread / task should stop - True to stop, false to continue running.
-
typedef std::function<bool(std::mutex &m, std::condition_variable &cv)> callback_m_cv_fn
Task callback function signature.
Note
The callback is run repeatedly within the Task, therefore it MUST return, and also SHOULD have a sleep to give the processor over to other tasks. For this reason, the callback is provided a std::condition_variable (and associated mutex) which the callback can use when they need to wait. If the cv.wait_for / cv.wait_until return std::cv_status::timeout, no action is necessary, but if they return std::cv_status::no_timeout, then the function should return immediately since the task is being stopped (optionally performing any task-specific tear-down).
Warning
This is an older callback function signature, and is kept for backwards compatibility. It is recommended to use the newer callback signature (callback_m_cv_notified_fn) which includes the notified parameter, enabling the task callback function to wait on the condition variable and ignore spurious wakeups.
- Param m
mutex associated with the condition variable (should be locked before calling cv.wait_for() or cv.wait_until())
- Param cv
condition variable the callback can use to perform an interruptible wait.
- Return
Whether or not the callback’s thread / task should stop - True to stop, false to continue running.
-
typedef std::function<bool()> callback_no_params_fn
Simple callback function signature.
Note
The callback is run repeatedly within the Task, therefore it MUST return, and also SHOULD have a sleep to give the processor over to other tasks.
- Return
True to stop the task, false to continue running.
-
typedef std::variant<callback_m_cv_notified_fn, callback_m_cv_fn, callback_no_params_fn> callback_variant
Variant of the callback function for the task.
This is primarily used to enable simpler API upgrades in the future and maximize backwards compatibility.
Note
This is a std::variant of the different callback function signatures that can be used with the Task. This allows the Task to be configured with a callback function that takes no parameters, a callback function that takes a mutex and condition variable, or a callback function that takes a mutex, condition variable, and a bool reference to the notified flag.
Public Functions
-
~Task()
Destroy the task, stopping it if it was started.
-
bool start()
Start executing the task.
- Returns
true if the task started, false if it was already started.
-
bool stop()
Stop the task execution.
This will request the task to stop, notify the condition variable, and (if this calling context is not the task context) join the thread.
- Returns
true if the task stopped, false if it was not started / already stopped.
-
bool is_started() const
Has the task been started or not?
- Returns
true if the task is started / running, false otherwise.
-
bool is_running() const
Is the task running?
- Returns
true if the task is running, false otherwise.
-
bool start_watchdog()
Start the task watchdog for this task.
Note
This function is only available on ESP
Note
This function will do nothing unless CONFIG_ESP_TASK_WDT_EN is enabled in the menuconfig. Default is y (enabled).
- Returns
true if the watchdog was started, false otherwise.
-
bool stop_watchdog()
Stop the task watchdog for this task.
Note
This function is only available on ESP
Note
This function will do nothing unless CONFIG_ESP_TASK_WDT_EN is enabled in the menuconfig. Default is y (enabled).
- Returns
true if the watchdog was stopped, false otherwise.
-
inline task_id_t get_id() const
Get the ID for this Task’s thread / task context.
- Returns
ID for this Task’s thread / task context.
-
inline const std::string &get_name() const
Get the name of the component
Note
This is the tag of the logger
- Returns
A const reference to the name of the component
-
inline void set_log_tag(const std::string_view &tag)
Set the tag for the logger
- Parameters
tag – The tag to use for the logger
-
inline espp::Logger::Verbosity get_log_level() const
Get the log level for the logger
See also
See also
- Returns
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- Parameters
level – The verbosity level to use for the logger
-
inline void set_log_verbosity(espp::Logger::Verbosity level)
Set the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls set_log_level
- Parameters
level – The verbosity level to use for the logger
-
inline espp::Logger::Verbosity get_log_verbosity() const
Get the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls get_log_level
- Returns
The verbosity level of the logger
-
inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)
Set the rate limit for the logger
See also
Note
Only calls to the logger that have _rate_limit suffix will be rate limited
- Parameters
rate_limit – The rate limit to use for the logger
Public Static Functions
-
static std::unique_ptr<Task> make_unique(const espp::Task::Config &config)
Get a unique pointer to a new task created with
config
. Useful to not have to use templated std::make_unique (less typing).
-
static bool configure_task_watchdog(uint32_t timeout_ms, bool panic_on_timeout = true)
Initialize/Configure the task watchdog for the current task.
Note
This function is only available on ESP
Note
This function will do nothing unless CONFIG_ESP_TASK_WDT_EN is enabled in the menuconfig. Default is y (enabled).
Note
This function will not monitor the idle tasks.
Note
If the watchdog has not been configured, then this function will call
esp_task_wdt_init
, otherwise it will then callesp_task_wdt_reconfigure
.- Parameters
timeout_ms – Timeout in milliseconds for the watchdog.
panic_on_timeout – Whether or not to panic on timeout.
- Returns
true if the watchdog was initialized, false otherwise.
-
static bool configure_task_watchdog(const std::chrono::milliseconds &timeout, bool panic_on_timeout = true)
Initialize/Configure the task watchdog for the current task.
Note
This function is only available on ESP
Note
This function will do nothing unless CONFIG_ESP_TASK_WDT_EN is enabled in the menuconfig. Default is y (enabled).
Note
This function will not monitor the idle tasks.
Note
If the watchdog has not been configured, then this function will call
esp_task_wdt_init
, otherwise it will then callesp_task_wdt_reconfigure
.- Parameters
timeout – The timeout for the watchdog.
panic_on_timeout – Whether or not to panic on timeout.
- Returns
true if the watchdog was initialized, false otherwise.
-
static std::string get_watchdog_info(std::error_code &ec)
Retrieve the info about tasks / users which triggered the task watchdog timeout.
Note
This function is only available on ESP
Note
This function will do nothing unless CONFIG_ESP_TASK_WDT_EN is enabled in the menuconfig. Default is y (enabled).
Note
This function will only return info for tasks which are still registered with the watchdog. If you call this after you have called stop_watchdog() for a task, then even if the task triggered the watchdog timeout you will not get that information.
- Parameters
ec – Error code to set if there was an error retrieving the info.
- Returns
std::string containing the task watchdog info, or an empty string if there was no timeout or there was an error retrieving the info.
-
static std::string get_info()
Get the info (as a string) for the task of the current context.
Note
This function is only available on ESP
- Returns
std::string containing name, core ID, priority, and stack high water mark (B)
-
static std::string get_info(const Task &task)
Get the info (as a string) for the provided
task
.Note
This function is only available on ESP
- Parameters
task – Reference to the task for which you want the information.
- Returns
std::string containing name, core ID, priority, and stack high water mark (B)
-
static inline task_id_t get_current_id()
Get the ID for the current thread / task context.
- Returns
ID for the current thread / task context.
-
typedef std::function<bool(std::mutex &m, std::condition_variable &cv, bool ¬ified)> callback_m_cv_notified_fn