警告

您正在阅读的 ROS 2 文档版本已达到 EOL(生命周期结束),不再受官方支持。如果您想了解最新信息,请访问 Jazzy.

执行人

概述

ROS 2 中的执行管理是通过执行器的概念来阐述的。执行器使用底层操作系统的一个或多个线程来调用订阅、定时器、服务服务器、动作服务器等对传入消息和事件的回调。显式 Executor 类(在 executor.hpp 中的 executors.pyexecutor.h rclc)比 ROS 1 中的自旋机制提供了更多的执行管理控制,尽管基本的应用程序接口非常相似。

下面,我们将重点介绍 C++ 客户端库 rclcpp.

基本用途

在最简单的情况下,主线程通过调用 rclcpp::spin(..) 具体如下

int 主要(int 参数, 烧焦* 参数[])
{
   // 进行一些初始化。
   rclcpp::启动(参数, 参数);
   ...

   // 实例化一个节点。
   rclcpp::节点::SharedPtr 网站 = ...

   // 运行执行器。
   rclcpp::后旋(网站);

   // 关闭并退出。
   ...
   返回 0;
}

呼吁 自旋(节点) 基本上扩展为单线程执行器的实例化和调用,这是最简单的执行器:

rclcpp::执行人::单线程执行器 执行人;
执行人.添加节点(网站);
执行人.后旋();

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

../_images/executors_basic_principle.png

单线程执行器还被容器进程用于 部件即在创建和执行节点时没有明确的主函数。

执行人类型

目前,rclcpp 提供了三种 Executor 类型,它们都源自一个共享的父类:

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::节点::SharedPtr 节点1 = ...
rclcpp::节点::SharedPtr 节点2 = ...
rclcpp::节点::SharedPtr 节点3 = ...

rclcpp::执行人::静态单线程执行器 执行人;
执行人.添加节点(节点1);
执行人.添加节点(节点2);
执行人.添加节点(节点3);
执行人.后旋();

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

回调组

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

我的回调组 = 创建回调组(rclcpp::回调组类型::相互排斥);

rclcpp::订阅选项 选项;
选项.回调组 = 我的回调组;

我的订阅 = 创建订阅<;Int32>;("/topic";, rclcpp::传感器数据QoS(),
                                             回调, 选项);

所有未指定回调组而创建的订阅、定时器等都会分配给 默认回调组.可以通过 NodeBaseInterface::get_default_callback_group()Node.default_callback_group 在 rclpy 中。

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

  • 相互排斥: 该组的回调不得并行执行。

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

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

从 Galactic 开始,rclcpp 中 Executor 基类的接口通过一个新函数得到了完善 add_callback_group(..).这样就可以将回调组分配给不同的执行器。通过使用操作系统调度程序配置底层线程,特定回调的优先级可高于其他回调。例如,控制循环的订阅和定时器可优先于节点的所有其他订阅和标准服务。调度程序 examples_rclcpp_cbg_executor 软件包 提供了这一机制的演示。

调度语义

如果回调的处理时间短于消息和事件发生的周期,执行器基本上会按先进先出的顺序处理它们。但是,如果某些回调的处理时间较长,消息和事件就会在堆栈的下层排队。等待集机制向执行器报告的关于这些队列的信息非常少。具体来说,它只报告某个主题是否有任何消息。执行器利用这些信息以循环方式处理报文(包括服务和操作),但不是按先进先出的顺序。下面的流程图直观地展示了这种调度语义。

../_images/executors_scheduling_semantics.png

这种语义首次在 卡西尼等人在 ECRTS 2019 上发表的论文.(注:本文还解释了定时器事件的优先级高于所有其他报文。 Eloquent 取消了这一优先级。)

展望

虽然 rclcpp 的三个执行器能很好地满足大多数应用程序的要求,但也存在一些问题,使它们不适合实时应用程序,因为实时应用程序需要明确定义的执行时间、确定性和对执行顺序的自定义控制。下面是其中一些问题的总结:

  1. 复杂和混合的调度语义。理想情况下,你需要定义明确的调度语义来执行正式的时序分析。

  2. 回调可能会出现优先级倒置。优先级较高的回调可能会被优先级较低的回调阻塞。

  3. 无法明确控制回调执行顺序。

  4. 无法对特定主题的触发进行内置控制。

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

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

  • rclcpp WaitSet:""""""""""""等字样。 等待设置 类允许直接等待订阅、定时器、服务服务器、动作服务器等,而不是使用 Executor。它可用于实现确定性的、用户定义的处理序列,可能同时处理来自不同订阅的多条消息。执行器 examples_rclcpp_wait_set 软件包 提供了几个使用这种用户级等待集机制的示例。

  • rclc 执行人:该执行器来自 C 客户端库 rclc它允许用户对回调的执行顺序进行精细控制,并允许自定义触发条件来激活回调。此外,它还实现了逻辑执行时间(LET)语义的理念。

更多信息