当前位置:   article > 正文

【ROS2回调调度原理】调用 spin 时 ROS 2 都在做什么?_ros2 spin

ros2 spin

在调用 spin 节点的时候 ROS 2 都在做什么?这篇文章可以给你答案。

小鱼另外回答一个问题,如何让服务同时处理多个请求?由于ROS2 模型使用的是单线程执行器和互斥回调组,所以要想让服务同时处理多个请求,需要使用多线程执行器配合可重入回调组即可,要使用超过CPU核心数量的并行处理,需要修改 MultiThreadedExecutor 的线程参数为一个较大值。

ROS 2中的执行管理和执行器

在ROS 2中,执行管理由执行器(Executor)来处理。执行器使用底层操作系统的一个或多个线程来调用订阅、定时器、服务服务器、动作服务器等的回调,以响应传入的消息和事件。明确的执行器类(在rclcpp的 executor.hpp 中,rclpy的 executors.py 中,或者rclc的 executor.h 中)提供了比ROS 1中的spin机制更多的执行管理控制,尽管基本API非常相似。

接下来,我们将重点介绍 C++ 客户端库 rclcpp。

基本用法

在最简单的情况下,可以通过调用 rclcpp::spin(..) 来使用主线程处理节点的传入消息和事件,示例如下:

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

spin(node) 的调用基本上扩展为单线程执行器的实例化和调用,这是最简单的执行器:

rclcpp::executors::SingleThreadedExecutor executor;
executor.add_node(node);
executor.spin();
  • 1
  • 2
  • 3

通过调用执行器实例的 spin(),当前线程开始查询 rcl 和中间件层的传入消息和其他事件,并调用相应的回调函数,直到节点关闭。为了不影响中间件的 QoS 设置,在客户端库层不会将传入消息存储在队列中,而是在中间件中保持,直到由回调函数进行处理。(这是与 ROS 1 的一个重要区别。)一个 等待集 用于通知执行器中间件层上可用的消息,每个队列对应一个二进制标志。等待集还用于检测计时器到期的情况。

在这里插入图片描述

单线程执行器还被容器进程用于 组件,即在所有没有显式主函数的情况下创建和执行节点的情况下。

执行器的类型

目前,rclcpp 提供了三种执行器类型,它们派生自一个共享的父类

在这里插入图片描述

多线程执行器创建可配置数量的线程,以允许并行处理多个消息或事件。 静态单线程执行器根据订阅、计时器、服务服务器、操作服务器等优化了扫描节点结构的运行时成本。 添加节点时,它仅执行一次此扫描,而其他两个执行程序会定期扫描此类更改。 因此,静态单线程执行器应仅与在初始化期间创建所有订阅、计时器等的节点一起使用。

通过为每个节点调用add_node(.. ),可以使用所有三个执行者与多个节点一起使用。

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(node3);
executor.spin();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在上面的示例中,使用一个静态单线程执行者的一个线程来同时服务三个节点。 在多线程执行者的情况下,实际并行性取决于回调组。

回调组

ROS 2允许将节点的回调组织成组。在rclcpp中,可以通过Node类的 create_callback_group 函数创建这样一个回调组。在rclpy中,可以通过调用特定回调组类型的构造函数来完成相同的操作。回调组必须在节点的执行过程中保留(例如,作为类成员),否则执行者将无法触发回调。然后,在创建订阅、定时器等时可以指定此回调组,例如,通过订阅选项:

my_callback_group = create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);

rclcpp::SubscriptionOptions options;
options.callback_group = my_callback_group;

my_subscription = create_subscription<Int32>("/topic", rclcpp::SensorDataQoS(), callback, options);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

所有没有指定回调组的订阅、定时器等都被分配到默认回调组。可以通过 NodeBaseInterface::get_default_callback_group()(在rclcpp中)和 Node.default_callback_group(在rclpy中)查询默认回调组。

有两种类型的回调组,类型必须在实例化时指定:

  1. 互斥(Mutually exclusive): 该组的回调不能并行执行。
  2. 可重入(Reentrant): 该组的回调可以并行执行。

不同回调组的回调可以并行执行。多线程执行器使用其线程作为池,根据这些条件尽可能并行处理尽可能多的回调。有关如何有效使用回调组的提示,请参阅使用回调组

在rclcpp中,Executor基类还具有函数 add_callback_group(..),可以将回调组分配给不同的执行器。通过使用操作系统调度器配置底层线程,可以使特定的回调优先于其他回调。例如,可以将控制循环的订阅和定时器优先于节点的所有其他订阅和标准服务。examples_rclcpp_cbg_executor package 提供了该机制的演示。

调度语义

如果回调函数的处理时间短于消息和事件发生的周期,执行器基本上按照先进先出(FIFO)的顺序处理它们。然而,如果某些回调函数的处理时间较长,消息和事件将在堆栈的较低层次上排队。等待集机制对这些队列向执行器仅报告了非常少的信息。具体而言,它只报告某个主题是否有任何消息。执行器使用这些信息以循环轮询的方式处理消息(包括服务和动作),而不是按照先进先出的顺序。下面的流程图可视化了这种调度语义。

在这里插入图片描述

这种语义首次在2019年的ECRTS会议上由Casini等人在一篇论文中描述。 链接 (注意:该论文还解释了计时器事件优先于所有其他消息的优先级,这个优先级在 Eloquent 中被移除了。 链接

展望

虽然rclcpp的三个执行器在大多数应用中运行良好,但存在一些问题,使它们不适用于实时应用程序,实时应用程序需要定义明确的执行时间、确定性和对执行顺序的自定义控制。以下是其中一些问题的摘要:

复杂和混合的调度语义。理想情况下,您希望有明确定义的调度语义以进行形式化的定时分析。

回调函数可能会受到优先级反转的影响。较高优先级的回调函数可能会被较低优先级的回调函数阻塞。

无法明确控制回调函数的执行顺序。

没有内置的机制来触发特定主题的回调函数。

此外,执行器在CPU和内存使用方面的开销相当大。静态单线程执行器大大减少了这种开销,但对于某些应用程序可能还不够。

这些问题已经部分得到以下改进的解决:

rclcpp WaitSet :rclcpp 的 WaitSet 类允许直接等待订阅、定时器、服务服务器、动作服务器等,而无需使用执行器。它可以用于实现确定性的、用户定义的处理序列,可能同时处理来自不同订阅的多个消息。 examples_rclcpp_wait_set package 提供了使用此用户级等待集机制的多个示例。

rclc Executor:这个来自C客户端库rclc的Executor是为micro-ROS开发的,它可以让用户对回调的执行顺序进行细粒度控制,并允许自定义触发条件来激活回调。此外,它实现了逻辑执行时间(LET)语义的概念。"

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/797780
推荐阅读
相关标签
  

闽ICP备14008679号