赞
踩
由于ROS2中的节点是C++对象,因此一个进程可以有多个节点。事实上,在许多情况下,这样做是非常有益的,因为当通信处于同一进程中时,可以通过使用共享内存策略来加速通信。另一个好处是,如果节点都在同一个程序中,它可以简化节点的部署。缺点是,一个节点中的故障可能会导致同一进程的所有节点终止。ROS2提供了几种在同一进程中运行多个节点的方法。最推荐的是使用执行器。
概述
ROS 2中的执行管理由执行者的概念来解释。执行器使用底层操作系统的一个或多个线程来调用订阅、计时器、服务服务器、动作服务器等对传入消息和事件的回调。显式Executor类(在rclcpp中的Executor.hpp中,在rclpy中的executions.py中,或在rclc中的executer.h中)提供了比ROS1中的自旋机制更多的执行管理控制,尽管基本API非常相似。
使用模板如下:
- int main(int argc, char* argv[])
- {
- // Some initialization.
- rclcpp::init(argc, argv);
- ...
-
- // Instantiate a node.
- rclcpp::Node::SharedPtr node = ...
-
- // Run the executor.
- rclcpp::spin(node);
-
- // Shutdown and exit.
- ...
- return 0;
- }
- rclcpp::executors::SingleThreadedExecutor executor;
- executor.add_node(node);
- executor.spin();
通过调用Executor实例的spin(),当前线程开始向rcl和中间件层查询传入消息和其他事件,并调用相应的回调函数,直到节点关闭。为了不抵消中间件的QoS设置,传入消息不存储在客户端库层的队列中,而是保留在中间件中,直到回调函数处理该消息。(这是与ROS 1的一个关键区别。)等待集用于通知执行器中间件层上的可用消息,每个队列有一个二进制标志。等待集还用于检测计时器何时过期。
单线程执行器也被容器进程用于组件,即在创建和执行节点时没有显式主函数的所有情况下。
- rclcpp::Node::SharedPtr node1 = ...
- rclcpp::Node::SharedPtr node2 = ...
- rclcpp::Node::SharedPtr node3 = ...
-
- rclcpp::executors::StaticSingleThreadedExecutor executor;
- executor.add_node(node1);
- executor.add_node(node2);
- executor.add_node(node2);
- executor.spin();
多线程执行器创建可配置数量的线程,以允许并行处理多个消息或事件。静态单线程执行器优化了在订阅、计时器、服务服务器、动作服务器等方面扫描节点结构的运行时成本。它只在添加节点时执行一次扫描,而其他两个执行器定期扫描此类更改。因此,静态单线程执行器只能用于在初始化期间创建所有订阅、计时器等的节点。
案例
单线程:
- #include "rclcpp/rclcpp.hpp"
-
- #include "std_msgs/msg/int32.hpp"
-
- using namespace std::chrono_literals;
- using std::placeholders::_1;
-
- class PublisherNode : public rclcpp::Node
- {
- public:
- PublisherNode()
- : Node("publisher_node")
- {
- publisher_ = create_publisher<std_msgs::msg::Int32>("int_topic", 10);
- timer_ = create_wall_timer(
- 500ms, std::bind(&PublisherNode::timer_callback, this));
- }
-
- void timer_callback()
- {
- message_.data += 1;
- publisher_->publish(message_);
- }
-
- private:
- rclcpp::Publisher<std_msgs::msg::Int32>::SharedPtr publisher_;
- rclcpp::TimerBase::SharedPtr timer_;
- std_msgs::msg::Int32 message_;
- };
-
- class SubscriberNode : public rclcpp::Node
- {
- public:
- SubscriberNode()
- : Node("subscriber_node")
- {
- subscriber_ = create_subscription<std_msgs::msg::Int32>(
- "int_topic", 10,
- std::bind(&SubscriberNode::callback, this, _1));
- }
-
- void callback(const std_msgs::msg::Int32::SharedPtr msg)
- {
- RCLCPP_INFO(get_logger(), "Hello %d", msg->data);
- }
-
- private:
- rclcpp::Subscription<std_msgs::msg::Int32>::SharedPtr subscriber_;
- };
-
- int main(int argc, char * argv[])
- {
- rclcpp::init(argc, argv);
-
- auto node_pub = std::make_shared<PublisherNode>();
- auto node_sub = std::make_shared<SubscriberNode>();
-
- rclcpp::executors::SingleThreadedExecutor executor;
- // rclcpp::executors::MultiThreadedExecutor executor(
- // rclcpp::executor::ExecutorArgs(), 8);
-
- executor.add_node(node_pub);
- executor.add_node(node_sub);
-
- executor.spin();
-
- rclcpp::shutdown();
- return 0;
- }
多线程:
- #include "rclcpp/rclcpp.hpp"
-
- #include "std_msgs/msg/int32.hpp"
-
- using namespace std::chrono_literals;
- using std::placeholders::_1;
-
- class PublisherNode : public rclcpp::Node
- {
- public:
- PublisherNode()
- : Node("publisher_node")
- {
- publisher_ = create_publisher<std_msgs::msg::Int32>("int_topic", 10);
- timer_ = create_wall_timer(
- 500ms, std::bind(&PublisherNode::timer_callback, this));
- }
-
- void timer_callback()
- {
- message_.data += 1;
- publisher_->publish(message_);
- }
-
- private:
- rclcpp::Publisher<std_msgs::msg::Int32>::SharedPtr publisher_;
- rclcpp::TimerBase::SharedPtr timer_;
- std_msgs::msg::Int32 message_;
- };
-
- class SubscriberNode : public rclcpp::Node
- {
- public:
- SubscriberNode()
- : Node("subscriber_node")
- {
- subscriber_ = create_subscription<std_msgs::msg::Int32>(
- "int_topic", 10,
- std::bind(&SubscriberNode::callback, this, _1));
- }
-
- void callback(const std_msgs::msg::Int32::SharedPtr msg)
- {
- RCLCPP_INFO(get_logger(), "Hello %d", msg->data);
- }
-
- private:
- rclcpp::Subscription<std_msgs::msg::Int32>::SharedPtr subscriber_;
- };
-
- int main(int argc, char * argv[])
- {
- rclcpp::init(argc, argv);
-
- auto node_pub = std::make_shared<PublisherNode>();
- auto node_sub = std::make_shared<SubscriberNode>();
-
- // rclcpp::executors::SingleThreadedExecutor executor;
- rclcpp::executors::MultiThreadedExecutor executor(
- rclcpp::executor::ExecutorArgs(), 8);
-
- executor.add_node(node_pub);
- executor.add_node(node_sub);
-
- executor.spin();
-
- rclcpp::shutdown();
- return 0;
- }
调度语义
如果回调的处理时间短于消息和事件发生的时间,则执行器基本上按FIFO顺序处理它们。但是,如果某些回调的处理时间较长,消息和事件将在堆栈的较低层排队。等待集机制只向执行器报告关于这些队列的很少信息。详细地说,它只报告是否有关于某个主题的消息。执行器使用此信息以循环方式处理消息(包括服务和操作),但不按FIFO顺序。下面的流程图可视化了这种调度语义。
小结
虽然rclcpp的三个执行器在大多数应用程序中运行良好,但存在一些问题,使它们不适合实时应用程序,因为实时应用程序需要定义良好的执行时间、确定性和对执行顺序的自定义控制。以下是其中一些问题的摘要:
复杂和混合的调度语义。理想情况下,需要定义良好的调度语义来执行正式的时序分析。
回调可能会发生优先级反转。较高优先级回调可能被较低优先级回调阻止。
对回调执行顺序没有显式控制。
对特定主题的触发没有内置控制。
此外,在CPU和内存使用方面,执行器开销相当大。静态单线程执行器大大减少了这一开销,但对于某些应用程序来说可能还不够。
部分解决了这些问题:
rclcpp WaitSet:rclcpp的WaitSet类允许直接在订阅、计时器、服务服务器、操作服务器等上等待,而不是使用Executor。它可以用于实现确定性的、用户定义的处理序列,可能同时处理来自不同订阅的多个消息。examples_rclp_wait_set包提供了使用此用户级等待集机制的几个示例。
rclc执行器:该执行器来自为micro-ROS开发的C客户端库rclc,为用户提供了对回调执行顺序的细粒度控制,并允许自定义触发条件来激活回调。此外,它实现了逻辑执行时间(LET)语义的思想。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。