ROS2机器人编程简述humble-第二章-Parameters .3.4
由于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)语义的思想。