ros2_control - rolling
Loading...
Searching...
No Matches
async_function_handler.hpp
1// Copyright 2024 PAL Robotics S.L.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
16
17#ifndef REALTIME_TOOLS__ASYNC_FUNCTION_HANDLER_HPP_
18#define REALTIME_TOOLS__ASYNC_FUNCTION_HANDLER_HPP_
19
20#include <fmt/format.h>
21#include <fmt/ranges.h>
22#include <atomic>
23#include <cmath>
24#include <condition_variable>
25#include <functional>
26#include <limits>
27#include <memory>
28#include <mutex>
29#include <stdexcept>
30#include <string>
31#include <thread>
32#include <utility>
33#include <vector>
34
35#include "rclcpp/clock.hpp"
36#include "rclcpp/duration.hpp"
37#include "rclcpp/logging.hpp"
38#include "rclcpp/time.hpp"
39#include "realtime_tools/realtime_helpers.hpp"
40
42{
52{
53public:
54 enum Value : int8_t {
55 UNKNOWN = -1,
58 };
59
60 AsyncSchedulingPolicy() = default;
61 constexpr AsyncSchedulingPolicy(Value value) : value_(value) {} // NOLINT(runtime/explicit)
62 explicit AsyncSchedulingPolicy(const std::string & data_type)
63 {
64 if (data_type == "synchronized") {
65 value_ = SYNCHRONIZED;
66 } else if (data_type == "detached") {
67 value_ = DETACHED;
68 } else {
69 value_ = UNKNOWN;
70 }
71 }
72
73 operator Value() const { return value_; }
74
75 explicit operator bool() const = delete;
76
77 constexpr bool operator==(AsyncSchedulingPolicy other) const { return value_ == other.value_; }
78 constexpr bool operator!=(AsyncSchedulingPolicy other) const { return value_ != other.value_; }
79
80 constexpr bool operator==(Value other) const { return value_ == other; }
81 constexpr bool operator!=(Value other) const { return value_ != other; }
82
83 std::string to_string() const
84 {
85 switch (value_) {
86 case SYNCHRONIZED:
87 return "synchronized";
88 case DETACHED:
89 return "detached";
90 default:
91 return "unknown";
92 }
93 }
94
95 AsyncSchedulingPolicy from_string(const std::string & data_type)
96 {
97 return AsyncSchedulingPolicy(data_type);
98 }
99
100private:
101 Value value_ = UNKNOWN;
102};
103
130{
136 bool validate() const
137 {
138 if (thread_priority < 0 || thread_priority > 99) {
139 RCLCPP_ERROR(
140 logger, "Invalid thread priority: %d. It should be between 0 and 99.", thread_priority);
141 return false;
142 }
143 if (scheduling_policy == AsyncSchedulingPolicy::DETACHED) {
144 if (!clock) {
145 RCLCPP_ERROR(logger, "Clock must be set when using DETACHED scheduling policy.");
146 return false;
147 }
148 if (exec_rate == 0u) {
149 RCLCPP_ERROR(logger, "Execution rate must be set when using DETACHED scheduling policy.");
150 return false;
151 }
152 }
153 if (scheduling_policy == AsyncSchedulingPolicy::UNKNOWN) {
154 throw std::runtime_error(
155 "AsyncFunctionHandlerParams: scheduling policy is unknown. "
156 "Please set it to either 'synchronized' or 'detached'.");
157 }
158 if (trigger_predicate == nullptr) {
159 RCLCPP_ERROR(logger, "The parsed trigger predicate is not valid!");
160 return false;
161 }
162 for (const int & core : cpu_affinity_cores) {
163 if (core < 0) {
164 RCLCPP_ERROR(logger, "Invalid CPU core id: %d. It should be a non-negative integer.", core);
165 return false;
166 }
167 }
168 return true;
169 }
170
188 template <typename NodeT>
189 void initialize(NodeT & node, const std::string & prefix)
190 {
191 if (node->has_parameter(prefix + "thread_priority")) {
192 thread_priority = static_cast<int>(node->get_parameter(prefix + "thread_priority").as_int());
193 }
194 if (node->has_parameter(prefix + "cpu_affinity")) {
195 const auto cpu_affinity_param =
196 node->get_parameter(prefix + "cpu_affinity").as_integer_array();
197 for (const auto & core : cpu_affinity_param) {
198 cpu_affinity_cores.push_back(static_cast<int>(core));
199 }
200 }
201 if (node->has_parameter(prefix + "scheduling_policy")) {
202 scheduling_policy =
203 AsyncSchedulingPolicy(node->get_parameter(prefix + "scheduling_policy").as_string());
204 }
205 if (
206 scheduling_policy == AsyncSchedulingPolicy::DETACHED &&
207 node->has_parameter(prefix + "execution_rate")) {
208 const int execution_rate =
209 static_cast<int>(node->get_parameter(prefix + "execution_rate").as_int());
210 if (execution_rate <= 0) {
211 throw std::runtime_error(
212 "AsyncFunctionHandler: execution_rate parameter must be positive.");
213 }
214 exec_rate = static_cast<unsigned int>(execution_rate);
215 }
216 if (node->has_parameter(prefix + "wait_until_initial_trigger")) {
217 wait_until_initial_trigger =
218 node->get_parameter(prefix + "wait_until_initial_trigger").as_bool();
219 }
220 if (node->has_parameter(prefix + "print_warnings")) {
221 print_warnings = node->get_parameter(prefix + "print_warnings").as_bool();
222 }
223 if (node->has_parameter(prefix + "thread_name")) {
224 thread_name = node->get_parameter(prefix + "thread_name").as_string();
225 }
226 }
227
228 int thread_priority = 50;
229 std::vector<int> cpu_affinity_cores = {};
230 AsyncSchedulingPolicy scheduling_policy = AsyncSchedulingPolicy::SYNCHRONIZED;
231 unsigned int exec_rate = 0u;
232 rclcpp::Clock::SharedPtr clock = nullptr;
233 rclcpp::Logger logger = rclcpp::get_logger("AsyncFunctionHandler");
234 std::function<bool()> trigger_predicate = []() { return true; };
235 bool wait_until_initial_trigger = true;
236 bool print_warnings = true;
237 std::string thread_name = "";
238};
239
245template <typename T>
247{
248public:
249 AsyncFunctionHandler() = default;
250
252
254
260 void init(
261 std::function<T(const rclcpp::Time &, const rclcpp::Duration &)> callback,
262 int thread_priority = 50)
263 {
264 if (callback == nullptr) {
265 throw std::runtime_error(
266 "AsyncFunctionHandler: parsed function to call asynchronously is not valid!");
267 }
268 if (thread_.joinable()) {
269 throw std::runtime_error(
270 "AsyncFunctionHandler: Cannot reinitialize while the thread is "
271 "running. Please stop the async callback first!");
272 }
273 async_function_ = callback;
274 thread_priority_ = thread_priority;
275 }
276
278
291 void init(
292 std::function<T(const rclcpp::Time &, const rclcpp::Duration &)> callback,
293 std::function<bool()> trigger_predicate, int thread_priority = 50)
294 {
295 if (trigger_predicate == nullptr) {
296 throw std::runtime_error("AsyncFunctionHandler: parsed trigger predicate is not valid!");
297 }
298 init(callback, thread_priority);
299 trigger_predicate_ = trigger_predicate;
300 }
301
302 void init(
303 std::function<T(const rclcpp::Time &, const rclcpp::Duration &)> callback,
304 const AsyncFunctionHandlerParams & params)
305 {
306 params.validate();
307 init(callback, params.trigger_predicate, params.thread_priority);
308 params_ = params;
309 pause_thread_ = params.wait_until_initial_trigger;
310 }
311
313
332 std::pair<bool, T> trigger_async_callback(
333 const rclcpp::Time & time, const rclcpp::Duration & period)
334 {
335 if (!is_initialized()) {
336 throw std::runtime_error("AsyncFunctionHandler: need to be initialized first!");
337 }
338 if (async_exception_ptr_) {
339 RCLCPP_ERROR(
340 params_.logger, "AsyncFunctionHandler: Exception caught in the async callback thread!");
341 std::rethrow_exception(async_exception_ptr_);
342 }
343 if (params_.scheduling_policy == AsyncSchedulingPolicy::DETACHED) {
344 RCLCPP_WARN_ONCE(
345 params_.logger,
346 "AsyncFunctionHandler is configured with DETACHED scheduling policy. "
347 "This means that the async callback may not be synchronized with the main thread. ");
348 if (pause_thread_.load(std::memory_order_relaxed)) {
349 {
350 std::unique_lock<std::mutex> lock(async_mtx_);
351 pause_thread_ = false;
352 RCLCPP_INFO(params_.logger, "AsyncFunctionHandler: Resuming the async callback thread.");
353 async_callback_return_ = T();
354 auto const sync_period = std::chrono::nanoseconds(1'000'000'000 / params_.exec_rate);
355 previous_time_ = params_.clock->now() - rclcpp::Duration(sync_period);
356 }
357 async_callback_condition_.notify_one();
358 }
359 return std::make_pair(true, async_callback_return_.load(std::memory_order_relaxed));
360 }
361 if (!is_running()) {
362 throw std::runtime_error(
363 "AsyncFunctionHandler: need to start the async callback thread first before triggering!");
364 }
365 std::unique_lock<std::mutex> lock(async_mtx_, std::try_to_lock);
366 bool trigger_status = false;
367 if (lock.owns_lock() && !trigger_in_progress_ && trigger_predicate_()) {
368 {
369 std::unique_lock<std::mutex> scoped_lock(std::move(lock));
370 trigger_in_progress_ = true;
371 current_callback_time_ = time;
372 current_callback_period_ = period;
373 }
374 async_callback_condition_.notify_one();
375 trigger_status = true;
376 }
377 const T return_value = async_callback_return_;
378 return std::make_pair(trigger_status, return_value);
379 }
380
382
385 T get_last_return_value() const { return async_callback_return_; }
386
388
391 const rclcpp::Time & get_current_callback_time() const { return current_callback_time_; }
392
394
397 const rclcpp::Duration & get_current_callback_period() const { return current_callback_period_; }
398
400
408 {
409 std::unique_lock<std::mutex> lock(async_mtx_);
410 stop_async_callback_ = false;
411 trigger_in_progress_ = false;
412 current_callback_time_ = rclcpp::Time(0, 0, RCL_CLOCK_UNINITIALIZED);
413 current_callback_period_ = rclcpp::Duration(0, 0);
414 last_execution_time_ = std::chrono::nanoseconds(0);
415 async_callback_return_ = T();
416 async_exception_ptr_ = nullptr;
417 }
418
420
424 {
425 if (is_running()) {
426 std::unique_lock<std::mutex> lock(async_mtx_);
427 cycle_end_condition_.wait(lock, [this] { return !trigger_in_progress_; });
428 return true;
429 }
430 return false;
431 }
432
434
442 {
443 RCLCPP_INFO_EXPRESSION(
444 params_.logger, !pause_thread_, "AsyncFunctionHandler: Pausing the async callback thread.");
445 if (params_.scheduling_policy == AsyncSchedulingPolicy::SYNCHRONIZED) {
446 pause_thread_ = true;
448 } else {
449 if (is_running()) {
450 pause_thread_.store(true, std::memory_order_relaxed);
451 std::unique_lock<std::mutex> lock(async_mtx_);
452 return true;
453 }
454 }
455 return pause_thread_.load(std::memory_order_relaxed);
456 }
457
459
462 bool is_initialized() const { return async_function_ && trigger_predicate_; }
463
465
470 {
471 if (is_running()) {
472 thread_.join();
473 }
474 }
475
477
480 bool is_running() const { return thread_.joinable(); }
481
483
486 bool is_stopped() const { return stop_async_callback_.load(std::memory_order_relaxed); }
487
489
492 bool is_paused() const { return pause_thread_.load(std::memory_order_relaxed); }
493
495
498 std::thread & get_thread() { return thread_; }
499
501
504 const std::thread & get_thread() const { return thread_; }
505
507
510 const AsyncFunctionHandlerParams & get_params() const { return params_; }
511
513
516 bool is_trigger_cycle_in_progress() const { return trigger_in_progress_; }
517
519
524 {
525 if (is_running()) {
526 {
527 stop_async_callback_.store(true, std::memory_order_relaxed);
528 std::unique_lock<std::mutex> lock(async_mtx_);
529 }
530 async_callback_condition_.notify_one();
531 thread_.join();
532 }
533 }
534
536
539 std::chrono::nanoseconds get_last_execution_time() const
540 {
541 return last_execution_time_.load(std::memory_order_relaxed);
542 }
543
545
551 {
552 if (!is_initialized()) {
553 throw std::runtime_error("AsyncFunctionHandler: need to be initialized first!");
554 }
555 if (!thread_.joinable()) {
557 thread_ = std::thread([this]() -> void {
558 if (!realtime_tools::configure_sched_fifo(thread_priority_)) {
559 RCLCPP_WARN(
560 params_.logger,
561 "Could not enable FIFO RT scheduling policy. Consider setting up your user to do FIFO "
562 "RT scheduling. See "
563 "[https://control.ros.org/master/doc/ros2_control/controller_manager/doc/userdoc.html] "
564 "for details.");
565 }
566 if (!params_.cpu_affinity_cores.empty()) {
567 const auto affinity_result =
568 realtime_tools::set_current_thread_affinity(params_.cpu_affinity_cores);
569 RCLCPP_WARN_EXPRESSION(
570 params_.logger, !affinity_result.first,
571 "Could not set CPU affinity for the async worker thread. Error: %s",
572 affinity_result.second.c_str());
573
574 std::string cores_list_print =
575 fmt::format("[{}]", fmt::join(params_.cpu_affinity_cores, ", "));
576
577 RCLCPP_WARN_EXPRESSION(
578 params_.logger, affinity_result.first,
579 "Async worker thread successfully pinned to Cores: %s", cores_list_print.c_str());
580 }
581 if (!params_.thread_name.empty()) {
582 const auto rename_result = realtime_tools::set_current_thread_name(params_.thread_name);
583
584 if (!rename_result.first) {
585 RCLCPP_WARN(
586 params_.logger, "Could not set thread name for the async worker thread. Error: %s",
587 rename_result.second.c_str());
588 } else {
589 RCLCPP_INFO(params_.logger, "%s", rename_result.second.c_str());
590 }
591 }
592 if (params_.scheduling_policy == AsyncSchedulingPolicy::SYNCHRONIZED) {
593 execute_synchronized_callback();
594 } else {
595 execute_detached_callback();
596 }
597 });
598 }
599 }
600
601private:
602 void execute_synchronized_callback()
603 {
604 while (!stop_async_callback_.load(std::memory_order_relaxed)) {
605 {
606 std::unique_lock<std::mutex> lock(async_mtx_);
607 async_callback_condition_.wait(
608 lock, [this] { return trigger_in_progress_ || stop_async_callback_; });
609 if (!stop_async_callback_) {
610 const auto start_time = std::chrono::steady_clock::now();
611 try {
612 async_callback_return_ =
613 async_function_(current_callback_time_, current_callback_period_);
614 } catch (...) {
615 async_exception_ptr_ = std::current_exception();
616 }
617 const auto end_time = std::chrono::steady_clock::now();
618 last_execution_time_ =
619 std::chrono::duration_cast<std::chrono::nanoseconds>(end_time - start_time);
620 }
621 trigger_in_progress_ = false;
622 }
623 cycle_end_condition_.notify_all();
624 }
625 }
626
627 void execute_detached_callback()
628 {
629 if (!params_.clock) {
630 throw std::runtime_error(
631 "AsyncFunctionHandler: Clock must be set when using DETACHED scheduling policy.");
632 }
633 if (params_.exec_rate == 0u) {
634 throw std::runtime_error(
635 "AsyncFunctionHandler: Execution rate must be set when using DETACHED scheduling policy.");
636 }
637
638 auto const period = std::chrono::nanoseconds(1'000'000'000 / params_.exec_rate);
639
640 if (pause_thread_) {
641 std::unique_lock<std::mutex> lock(async_mtx_);
642 async_callback_condition_.wait(
643 lock, [this] { return !pause_thread_ || stop_async_callback_; });
644 }
645 // for calculating the measured period of the loop
646 previous_time_ = params_.clock->now();
647 std::this_thread::sleep_for(period);
648 std::chrono::steady_clock::time_point next_iteration_time{std::chrono::steady_clock::now()};
649 while (!stop_async_callback_.load(std::memory_order_relaxed)) {
650 {
651 std::unique_lock<std::mutex> lock(async_mtx_);
652 async_callback_condition_.wait(
653 lock, [this] { return !pause_thread_ || stop_async_callback_; });
654 if (!stop_async_callback_) {
655 // calculate measured period
656 auto const current_time = params_.clock->now();
657 auto const measured_period = current_time - previous_time_;
658 previous_time_ = current_time;
659 current_callback_time_ = current_time;
660 current_callback_period_ = measured_period;
661
662 const auto start_time = std::chrono::steady_clock::now();
663 try {
664 async_callback_return_ = async_function_(current_time, measured_period);
665 } catch (...) {
666 async_exception_ptr_ = std::current_exception();
667 }
668 last_execution_time_ = std::chrono::duration_cast<std::chrono::nanoseconds>(
669 std::chrono::steady_clock::now() - start_time);
670
671 next_iteration_time += period;
672 const auto time_now = std::chrono::steady_clock::now();
673 if (next_iteration_time < time_now) {
674 const double time_diff =
675 std::chrono::duration<double, std::milli>(time_now - next_iteration_time).count();
676 const double cm_period = 1.e3 / static_cast<double>(params_.exec_rate);
677 const int overrun_count = static_cast<int>(std::ceil(time_diff / cm_period));
678 if (params_.print_warnings) {
679 RCLCPP_WARN_THROTTLE(
680 params_.logger, *params_.clock, 1000,
681 "Overrun detected! The async callback missed its desired rate of %d Hz. The loop "
682 "took %f ms (missed cycles : %d).",
683 params_.exec_rate, time_diff + cm_period, overrun_count + 1);
684 }
685 next_iteration_time += (overrun_count * period);
686 }
687 std::this_thread::sleep_until(next_iteration_time);
688 }
689 trigger_in_progress_ = false;
690 }
691 cycle_end_condition_.notify_all();
692 }
693 }
694
695 rclcpp::Time current_callback_time_ = rclcpp::Time(0, 0, RCL_CLOCK_UNINITIALIZED);
696 rclcpp::Duration current_callback_period_{0, 0};
697
698 std::function<T(const rclcpp::Time &, const rclcpp::Duration &)> async_function_;
699 std::function<bool()> trigger_predicate_ = []() { return true; };
700
701 // Async related variables
702 std::thread thread_;
703 AsyncFunctionHandlerParams params_;
704 rclcpp::Time previous_time_{0, 0, RCL_CLOCK_UNINITIALIZED};
705 int thread_priority_ = std::numeric_limits<int>::quiet_NaN();
706 std::atomic_bool stop_async_callback_{false};
707 std::atomic_bool trigger_in_progress_{false};
708 std::atomic_bool pause_thread_{false};
709 std::atomic<T> async_callback_return_;
710 std::condition_variable async_callback_condition_;
711 std::condition_variable cycle_end_condition_;
712 std::mutex async_mtx_;
713 std::atomic<std::chrono::nanoseconds> last_execution_time_;
714 std::atomic<double> periodicity_;
715 std::exception_ptr async_exception_ptr_;
716};
717} // namespace realtime_tools
718
719#endif // REALTIME_TOOLS__ASYNC_FUNCTION_HANDLER_HPP_
Class to handle asynchronous function calls. AsyncFunctionHandler is a class that allows the user to ...
Definition async_function_handler.hpp:247
bool wait_for_trigger_cycle_to_finish()
Waits until the current async callback method trigger cycle is finished.
Definition async_function_handler.hpp:423
std::pair< bool, T > trigger_async_callback(const rclcpp::Time &time, const rclcpp::Duration &period)
Triggers the async callback method cycle.
Definition async_function_handler.hpp:332
std::thread & get_thread()
Get the async worker thread.
Definition async_function_handler.hpp:498
bool is_trigger_cycle_in_progress() const
Check if the async callback method is in progress.
Definition async_function_handler.hpp:516
std::chrono::nanoseconds get_last_execution_time() const
Get the last execution time of the async callback method.
Definition async_function_handler.hpp:539
const rclcpp::Time & get_current_callback_time() const
Get the current callback time.
Definition async_function_handler.hpp:391
void start_thread()
Initializes and starts the callback thread.
Definition async_function_handler.hpp:550
void init(std::function< T(const rclcpp::Time &, const rclcpp::Duration &)> callback, std::function< bool()> trigger_predicate, int thread_priority=50)
Initialize the AsyncFunctionHandler with the callback, trigger_predicate and thread_priority.
Definition async_function_handler.hpp:291
const AsyncFunctionHandlerParams & get_params() const
Get the parameters used to configure the AsyncFunctionHandler.
Definition async_function_handler.hpp:510
void reset_variables()
Resets the internal variables of the AsyncFunctionHandler.
Definition async_function_handler.hpp:407
bool is_running() const
Check if the async worker thread is running.
Definition async_function_handler.hpp:480
bool is_paused() const
Check if the async callback thread is paused.
Definition async_function_handler.hpp:492
bool pause_execution()
Pauses the execution of the async callback thread.
Definition async_function_handler.hpp:441
const rclcpp::Duration & get_current_callback_period() const
Get the current callback period.
Definition async_function_handler.hpp:397
void init(std::function< T(const rclcpp::Time &, const rclcpp::Duration &)> callback, int thread_priority=50)
Initialize the AsyncFunctionHandler with the callback and thread_priority.
Definition async_function_handler.hpp:260
void stop_thread()
Stops the callback thread.
Definition async_function_handler.hpp:523
void join_async_callback_thread()
Join the async callback thread.
Definition async_function_handler.hpp:469
bool is_stopped() const
Check if the async callback is triggered to stop the cycle.
Definition async_function_handler.hpp:486
bool is_initialized() const
Check if the AsyncFunctionHandler is initialized.
Definition async_function_handler.hpp:462
const std::thread & get_thread() const
Get the const version of async worker thread.
Definition async_function_handler.hpp:504
T get_last_return_value() const
Get the last return value of the async callback method.
Definition async_function_handler.hpp:385
Enum class to define the scheduling policy for the async worker thread. SYNCHRONIZED: The async worke...
Definition async_function_handler.hpp:52
Value
Definition async_function_handler.hpp:54
@ DETACHED
Synchronized scheduling policy.
Definition async_function_handler.hpp:57
@ SYNCHRONIZED
Unknown scheduling policy.
Definition async_function_handler.hpp:56
A pthread mutex wrapper that provides a mutex with the priority inheritance protocol and a priority c...
Definition async_function_handler.hpp:42
std::pair< bool, std::string > set_current_thread_name(const std::string &name)
Definition realtime_helpers.cpp:281
std::pair< bool, std::string > set_current_thread_affinity(int core)
Definition realtime_helpers.cpp:263
bool configure_sched_fifo(int priority)
Definition realtime_helpers.cpp:74
The AsyncFunctionHandlerParams struct is used to configure the AsyncFunctionHandler....
Definition async_function_handler.hpp:130
void initialize(NodeT &node, const std::string &prefix)
Initialize the parameters from a node's parameters. The node should have the following parameters:
Definition async_function_handler.hpp:189
bool validate() const
Validate the parameters.
Definition async_function_handler.hpp:136