Ask Your Question
1

Problems with rclcpp executor as class member.

asked 2021-04-21 22:01:51 -0600

M@t gravatar image

updated 2021-04-21 22:04:20 -0600

The Problem (in short)

I am trying to write a large piece of code that needs to set up subscribers, look up transforms, and call spin_some, spin_once etc in the body of code. I am encountering a number of very vague and inconsistent crashes that I suspect are all tied back to the executor and how I'm using it. So, I am trying to establish the correct use of the executor as a class member, using the minimal compiling example below.

I'm using ROS2 Foxy for reference.

The Problem (in full)

Node executors are, in most examples I have found, used outside the body of the node, like so (from the ROS2 C++ subscriber tutorial):

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<MinimalNode>());
    rclcpp::shutdown();
    return 0;
}

I know that spin works under the hood by calling up an executor, assigning the node to it, and then spinning it. However I want to be able to call functions like spin, spin_once etc from within the body of the node. In the process of looking for a solution, I came up with this method:

#include <string>
#include "rclcpp/rclcpp.hpp"

class MinimalNode : public rclcpp::Node
{
    rclcpp::executors::SingleThreadedExecutor executor;

    public:
        MinimalNode() : Node("minimal_node")
        {
            executor.add_node(this->get_node_base_interface());
            executor.spin_some();
            printf("Hello world\n");
            executor.spin();
        }
};

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    auto node_ptr = std::make_shared<MinimalNode>();
    rclcpp::shutdown();
    return 0;
}

Where the executor exists as a class member, the constructor adds the node interface, calls node spin_some, spin etc. This works, it compiles, runs, prints "Hello world" and then spins quietly until I kill it with Ctrl+C. However things go sideways from here. Remember, this minimal example roughly replicates a large body of code which I cannot share publically.

My questions are:

  1. Is this a legitimate way to use the executor and achieve the goal of having the node be able to call things like spin internally? I haven't seen this use case in any of the example ROS2 code I can find. If not, what is the proper way to do this?
  2. If I comment out the executor.add_node line in this example, everything still works. But I'm not sure if it should, because how does the executor know what node it is supposed to spin?
  3. In my larger body of code, this doesn't even work. I get a crash at the executor.add_node line, and the error message: "Node has already been added to an executor." Why don't I get that error with the minimal example?
  4. In my larger body of code, it will compile and run ONLY if I comment out the executor.add_node line. But again, I don'tknow if this should work, and it only partially seems to work, because my node exists normally and doesn't stop at executor.spin();
  5. In my larger body of code, if I launch the node via launch file rather than via ros2 run... again ...
(more)
edit retag flag offensive close merge delete

1 Answer

Sort by ยป oldest newest most voted
0

answered 2021-10-31 22:18:34 -0600

BrettRD gravatar image

I'm still getting a grip on executors, they changed subtly fairly recently and there's a lot of indirection to make old design patterns just work (which occasionally gets in the way.)

As I understand it, executors allow you to split node callbacks into separate queues, and manipulate the execution order and threading model with very fine-grained control. You can have multiple executors, and I think you can put one callback into multiple executors. Docs are very thin on the ground, and very few public nodes seem to be complex enough to need control of their threading models.

Executors allow you to shard your node's subscriptions and timers into separate callback-groups, and then add the callback-groups to multiple executors. This gets deeply into thread-safety territory very quickly.

In https://github.com/BrettRD/ros-gst-br..., I use an executor to host the entire node and all of its callbacks because the thread is owned by gstreamer and needs to do other work. This means that referring to any information in the gstreamer class during a node (executor thread) callback is not thread-safe without a lock.

I think in 2. you're running into some of that automagic config stuff; An executor without a context argument makes a call to get_global_default_context() - the same context as the default-constructed node. I suspect that's the code-path that is silently adding your node to the default executor

edit flag offensive delete link more

Your Answer

Please start posting anonymously - your entry will be published after you log in or create a new account.

Add Answer

Question Tools

4 followers

Stats

Asked: 2021-04-21 22:01:51 -0600

Seen: 196 times

Last updated: Oct 31