执行

概述

ROS 2 中的执行管理由 Executors 处理。 Executor 使用底层操作系统的一个或多个线程来调用订阅、计时器、服务服务器、操作服务器等对传入消息和事件的回调。 显式 Executor 类(in executor.hpp in rclcpp, in executors.py in rclpy, or in executor.h in rclc) provides more control over execution management than the spin mechanism in ROS 1, although the basic API is very similar.

下面我们重点介绍 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;
}

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

rclcpp::executors::SingleThreadedExecutor executor;
executor.add_node(node);
executor.spin();

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

../../_images/executors_basic_principle.png

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

执行器类型

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

digraph Flatland {

   Executor -> SingleThreadedExecutor [dir = back, arrowtail = empty];
   Executor -> MultiThreadedExecutor [dir = back, arrowtail = empty];
   Executor -> StaticSingleThreadedExecutor [dir = back, arrowtail = empty];
   Executor  [shape=polygon,sides=4];
   SingleThreadedExecutor  [shape=polygon,sides=4];
   MultiThreadedExecutor  [shape=polygon,sides=4];
   StaticSingleThreadedExecutor  [shape=polygon,sides=4];

   }

多线程执行器 创建可配置数量的线程,以允许并行处理多个消息或事件。

静态单线程执行器 优化了扫描节点结构(包括订阅、计时器、服务服务器、操作服务器等)的运行时成本。

它仅在添加节点时执行一次此扫描,而其他两个执行器会定期扫描此类更改。

因此,静态单线程执行器应仅用于在初始化期间创建所有订阅、计时器等的节点。

通过为每个节点调用“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();

在上面的例子中,静态单线程执行器的一个线程用于同时为三个节点提供服务。 对于多线程执行器,实际的并行性取决于回调组。

回调组

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);

所有未指定回调组的订阅、计时器等均被分配到 默认回调组

可以通过 rclcpp 中的 NodeBaseInterface::get_default_callback_group() 和 rclpy 中的 Node.default_callback_group 查询默认回调组。

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

  • 互斥: 此组的回调不得并行执行。

  • 可重入: 此组的回调可以并行执行。

不同回调组的回调始终可以并行执行。

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

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

调度语义

如果回调的处理时间短于消息和事件发生的周期,则执行器基本上按 FIFO 顺序处理它们。 但是,如果某些回调的处理时间较长,则消息和事件将在堆栈的较低层排队。 等待集机制仅向执行器报告有关这些队列的极少量信息。 具体来说,它仅报告是否有某个主题的消息。 执行器使用此信息以循环方式处理消息(包括服务和操作) - 但不按 FIFO 顺序处理。 以下流程图可视化了此调度语义。

../../_images/executors_scheduling_semantics.png

这种语义最早是在 paper by Casini et al. at ECRTS 2019. (注:本文还解释了计时器事件优先于所有其他消息。This prioritization was removed in Eloquent.)

展望

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

1. 复杂且混合的调度语义。 理想情况下,您需要明确定义的调度语义来执行正式的时间分析。 2. 回调可能会遭受优先级反转。 优先级较高的回调可能会被优先级较低的回调阻止。 3. 没有对回调执行顺序的明确控制。 4. 没有对特定主题触发的内置控制。

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

这些问题已通过以下开发得到部分解决:

  • rclcpp WaitSet: The WaitSet class of rclcpp allows waiting directly on subscriptions, timers, service servers, action servers, etc. instead of using an Executor. It can be used to implement deterministic, user-defined processing sequences, possibly processing multiple messages from different subscriptions together. The examples_rclcpp_wait_set package provides several examples for the use of this user-level wait set mechanism.

  • rclc Executor: This Executor from the C Client Library rclc, developed for micro-ROS, gives the user fine-grained control over the execution order of callbacks and allows for custom trigger conditions to activate callbacks. Furthermore, it implements ideas of the Logical Execution Time (LET) semantics.

Further information