执行者
概述
ROS 2 中的执行管理由执行器处理。 执行器使用底层操作系统的一个或多个线程来调用对传入消息和事件的订阅、计时器、服务服务器、操作服务器等的回调。 显式 Executor 类(在 rclcpp 中的 executor.hpp 中,在 rclpy 中的 executors.py 中,或在 rclc 中的 executor.h 中)比 ROS 1 中的旋转机制提供了对执行管理的更多控制,尽管基本 API 非常相似。
下面,我们重点介绍 C++ 客户端库 rclcpp。
基本用法
最简单的情况下,主线程用于处理 Node 的传入消息和事件,方法是调用“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 中间件层上的可用消息,每个队列一个二进制标志。 *等待集*还用于检测计时器何时到期。
单线程执行器还被容器进程用于:doc:components,即在所有没有显式主函数的情况下创建和执行节点的情况。
执行器类型
目前,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();
在上面的例子中,静态单线程执行器的一个线程用于同时为三个节点提供服务。 对于多线程执行器,实际的并行性取决于回调组。
回调组
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);
my_callback_group = MutuallyExclusiveCallbackGroup()
my_subscription = self.create_subscription(Int32, "/topic", self.callback, qos_profile=1,
callback_group=my_callback_group)
所有未指定回调组的订阅、计时器等均被分配到 默认回调组。
可以通过 rclcpp 中的 NodeBaseInterface::get_default_callback_group()
和 rclpy 中的 Node.default_callback_group
查询默认回调组。
回调组有两种类型,必须在实例化时指定类型:
互斥: 此组的回调不得并行执行。
可重入: 此组的回调可以并行执行。
不同回调组的回调始终可以并行执行。
多线程执行器使用其线程作为池,根据这些条件并行处理尽可能多的回调。 有关如何有效使用回调组的提示,请参阅:doc:使用回调组。
rclcpp 中的 Executor 基类还具有函数 add_callback_group(..)
,该函数允许将回调组分发到不同的 Executor。
通过使用操作系统调度程序配置底层线程,可以将特定回调优先于其他回调。
例如,控制循环的订阅和计时器可以优先于节点的所有其他订阅和标准服务。
examples_rclcpp_cbg_executor 包 提供了此机制的演示。
调度语义
如果回调的处理时间短于消息和事件发生的周期,则执行器基本上按 FIFO 顺序处理它们。 但是,如果某些回调的处理时间较长,则消息和事件将在堆栈的较低层排队。 等待集机制仅向执行器报告有关这些队列的极少量信息。 具体来说,它仅报告是否有某个主题的消息。 执行器使用此信息以循环方式处理消息(包括服务和操作) - 但不按 FIFO 顺序处理。 以下流程图可视化了此调度语义。
这种语义最初是在“Casini 等人在 ECRTS 2019 上的一篇论文 <https://drops.dagstuhl.de/opus/volltexte/2019/10743/pdf/LIPIcs-ECRTS-2019-6.pdf>”中描述的。 (注意:该论文还解释了计时器事件优先于所有其他消息。“这种优先级在 Eloquent 中被删除了。<https://github.com/ros2/rclcpp/pull/841>”)
展望
虽然 rclcpp 的三个执行器适用于大多数应用程序,但有些问题使它们不适合实时应用程序,这些应用程序需要明确定义的执行时间、确定性和对执行顺序的自定义控制。 以下是其中一些问题的摘要:
1. 复杂且混合的调度语义。 理想情况下,您需要明确定义的调度语义来执行正式的时间分析。 2. 回调可能会遭受优先级反转。 优先级较高的回调可能会被优先级较低的回调阻止。 3. 无法明确控制回调的执行顺序。 4. 没有内置控制特定主题的触发。
此外,执行器在 CPU 和内存使用方面的开销相当大。 静态单线程执行器大大降低了这种开销,但对于某些应用程序来说可能还不够。
这些问题已通过以下开发得到部分解决:
rclcpp WaitSet: rclcpp 的
WaitSet
类允许直接等待订阅、计时器、服务服务器、动作服务器等,而不是使用 Executor。 它可用于实现确定性的、用户定义的处理序列,可能一起处理来自不同订阅的多条消息。 The examples_rclcpp_wait_set package 提供了几个使用该用户级等待集机制的示例。rclc Executor: 此执行器来自为微型 ROS 开发的 C 客户端库 rclc,它为用户提供了对回调执行顺序的细粒度控制,并允许自定义触发条件来激活回调。 此外,它实现了逻辑执行时间(LET)语义的思想。
更多信息
Michael Pöhnl 等人:“ROS 2 执行器:如何使其高效、实时和确定性?”<https://www.apex.ai/roscon-21>`_。ROS World 2021 研讨会。虚拟活动。2021 年 10 月 19 日。
Ralph Lange:“使用 ROS 2 进行高级执行管理”<https://www.youtube.com/watch?v=Sz-nllmtcc8&t=109s>`_。ROS 工业会议。虚拟活动。 2020 年 12 月 16 日。
Daniel Casini、Tobias Blass、Ingo Lütkebohle 和 Björn Brandenburg:“基于预留的调度下 ROS 2 处理链的响应时间分析”<https://drops.dagstuhl.de/opus/volltexte/2019/10743/pdf/LIPIcs-ECRTS-2019-6.pdf>`_,第 31 届 ECRTS 2019 会议论文集,德国斯图加特,2019 年 7 月。