Executor and Spin Explained
This tutorial is based on ROS2 Humble and the behavior described in this tutorial is subject to change in future releases.
Related Readings
Introduction
In this tutorial, we aim to clarify the difference between spin_once, spin_some, spin_until_future_complete, and spin. However, due to limited official documentation, the accuracy of our explanations and interpretations is not guaranteed. We've include code at the end of the tutorial for you to independently verify the behaviors. Please note, this content is specifically tailored to ROS2 Humble; other version may exhibit different behaviors.
A Few Words on Concurrency
It's highly recommended to read the discussion ROS2: multi nodes, each on a thread in same process.
By default, ROS 2 is thread safe:
Each node's callbacks are called mutually exclusive (i.e. only one callback per node is called at a time, there is no concurrence within a node),
Different nodes' callbacks can be executed concurrently:
If they run in different processes, or
If they are in the same process and spun by a MultiThreadedExecutor, or
If they are in the same process and you use multiple threads each running a SingleThreadedExecutor,
But even in those cases, the active callbacks cannot access another nodes' data, and only one callback per node is being executed, so there are no concurrency issues.
In this tutorial, we will not focus on the callback group.
Experiment Setup
The experimental system consists of
A publisher that simultaneously publishes the same messages to two different topics.
A subscriber that subscribes to these two topics. The callback function is used to simulate the task execution and the execution time can be adjusted via a parameter.
Parameters that control the start-up time of the publisher and subscriber. If the subscriber activates before the publisher, the the executor queue is empty because no messages are published yet. Conversely, if the publisher starts first, the subscriber may see a non-empty queue.
Parameters that control the message publication rate and task execution time. An execution time longer than the publication interval leads to buildup of tasks in the executor queue. Conversely, if the execution time is shorter, the queue gets drained.
Spin Once and Spin Some
spin_once and spin_some have similar behaviors. Upon invocation, the executor inspects the work queue. If the queue is empty, the function returns immediately. If tasks are present, spin_once executes a single task, whereas spin_some can handle one or more tasks. It's important to note that although new tasks may arrive during execution, the execution will not address them in the current cycle since task collection occurs only once.
Experiment 1
In this experiment, we demonstrate the call returns immediately if there is no work in the queue. We will introduce a delay in starting the publisher. Consequently, when the subscriber is initiated, no messages have been published yet, resulting in an empty work queue.
First, let's launch the system with spin_once:
Here is the output of the program:
The log message confirms that the subscriber is created before the first message is published. Moreover, the spin_once call returns immediately.
Let's try spin_some:
Similar results are produced:
Experiment 2
In this experiment, we demonstrate the difference between spin_once and spin_some. Specifically, spin_once only processes a single task in the queue while spin_some can handle multiple tasks. To illustrate this, we start the publisher before the subscribing, ensuring that by the time the subscriber is initiated, messages have already been published to the topics. Additionally, the message publication rate is set to be slower than the rate at which tasks can be competed.
Let's start with spin_once:
As we can see in the output below, between two calls of spin_once, at most one callback is executed:
Now let's check the behavior of spin_some:
Here is the output:
Let's examine the log more closely. The snippet below shows that between two spin_some calls, two task 3 are executed. This is expected since the same message is published to two topics. This shows spin_some can execute multiple tasks.
Interestingly, despite using a multi-thread executor, the two tasks are executed sequentially. This behavior suggests that the system operates as though only a single thread is allocated to the node.
We also notice that the interval between the two spin_some call is approximately 2 seconds, aligning with the combined execution time of two tasks, where each takes about 1 second. This observation highlights the "blocking" nature of the spin_some method.
Experiment 3
This experiment, focused on spin_some, demonstrates that the executor collects work only once per spin_some call. We configure tasks to run for 3 seconds and publish messages every 1 second. Given that work arrives faster than it can be processed, we will see accumulation of tasks in the queue.
Launch the system using the following command:
It produces the following outputs: (the key is that there are multiple call spin_some messages)
Spin Until Future is Complete
In the previous section, we observed that both spin_once and spin_some methods return either when the queue is empty or after the collected work is done. In particular, experiment 3 shows that spin_some returns even when there is more work in the queue. You may wonder how can we keep the executor continuously working on new tasks in the queue?
One approach involves manually wrapping the spin_once and spin_some methods within a loop. Alternatively, you can use the built-in methods spin_until_future_complete or spin. The difference between spin_until_future_complete and spin methods is that the "focus time" is bounded by the future argument in spin_until_future_complete whereas the "focus time" is unbounded in spin.
Experiment 4
In this experiment, we run a long-running task in a future and pass it to the spin_until_future_complete method. Due to a pending issue, additional setup is required to wake up the executor, allowing the executor to exit from the spin_until_future_complete method.
To launch the experiment, use the command below:
The output of the program is presented as follows:
The key part in the output is highlighted in the section below:
It shows that the spin_until_future_complete exits when the work in the future is done.
Spin
spin is the most common one among these variants. According to the code comments, it does work periodically as it becomes available to the executor. It's a blocking call and may block indefinitely. Unlike other method, the spin method does not return when the work queue is empty. It always waits for additional tasks.
Download Code
The code is available for download at this link.
©2023 - 2024 all rights reserved
Last updated