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 .
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.
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [publisher-1]: process started with pid [106002]
[INFO] [spin_some_repeat-2]: process started with pid [106004]
[spin_some_repeat-2] [INFO] [1703721861.285776631] [subscriber-main-thread]: ----- start to create the subscriber node
[spin_some_repeat-2] [INFO] [1703721862.291873202] [subscriber-main-thread]: ----- subscriber node is created
[spin_some_repeat-2] [INFO] [1703721862.292095052] [subscriber-main-thread]: ----- start spin_some
[spin_some_repeat-2] [INFO] [1703721862.292110432] [subscriber-main-thread]: ----- call spin_some
[spin_some_repeat-2] [INFO] [1703721863.292420633] [subscriber-main-thread]: ----- call spin_some
[spin_some_repeat-2] [INFO] [1703721864.292675445] [subscriber-main-thread]: ----- call spin_some
[spin_some_repeat-2] [INFO] [1703721865.292924385] [subscriber-main-thread]: ----- call spin_some
[spin_some_repeat-2] [INFO] [1703721866.293178492] [subscriber-main-thread]: ----- call spin_some
[spin_some_repeat-2] [INFO] [1703721867.293437536] [subscriber-main-thread]: ----- call spin_some
[spin_some_repeat-2] [INFO] [1703721868.293725006] [subscriber-main-thread]: ----- call spin_some
[spin_some_repeat-2] [INFO] [1703721869.293870219] [subscriber-main-thread]: ----- call spin_some
[spin_some_repeat-2] [INFO] [1703721870.294132843] [subscriber-main-thread]: ----- call spin_some
[publisher-1] [INFO] [1703721871.288962120] [task_publisher]: creating publisher and timer
[spin_some_repeat-2] [INFO] [1703721871.294388290] [subscriber-main-thread]: ----- call spin_some
[publisher-1] [INFO] [1703721872.290757235] [task_publisher]: Publishing: 'task 1'
[spin_some_repeat-2] [INFO] [1703721872.294619692] [subscriber-main-thread]: ----- spin_some ends
[INFO] [spin_some_repeat-2]: process has finished cleanly [pid 106004]
[publisher-1] [INFO] [1703721873.290692259] [task_publisher]: Publishing: 'task 2'
[publisher-1] [INFO] [1703721874.290768198] [task_publisher]: Publishing: 'task 3'
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.
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [publisher-1]: process started with pid [106414]
[INFO] [spin_some_repeat-2]: process started with pid [106416]
[spin_some_repeat-2] [INFO] [1703722411.352279678] [subscriber-main-thread]: ----- start to create the subscriber node
[publisher-1] [INFO] [1703722412.355485901] [task_publisher]: creating publisher and timer
[publisher-1] [INFO] [1703722415.357177825] [task_publisher]: Publishing: 'task 1'
[publisher-1] [INFO] [1703722418.357209205] [task_publisher]: Publishing: 'task 2'
[spin_some_repeat-2] [INFO] [1703722421.357183365] [subscriber-main-thread]: ----- subscriber node is created
[publisher-1] [INFO] [1703722421.357189710] [task_publisher]: Publishing: 'task 3'
[spin_some_repeat-2] [INFO] [1703722421.357378437] [subscriber-main-thread]: ----- start spin_some
[spin_some_repeat-2] [INFO] [1703722421.357391962] [subscriber-main-thread]: ----- call spin_some
[spin_some_repeat-2] [INFO] [1703722422.357763617] [subscriber-main-thread]: ----- call spin_some
[spin_some_repeat-2] [INFO] [1703722422.357974239] [task_subscriber]: start the task task 3
[spin_some_repeat-2] [INFO] [1703722423.358207789] [task_subscriber]: task is complete task 3
[spin_some_repeat-2] [INFO] [1703722423.358434326] [task_subscriber]: start the task task 3
[publisher-1] [INFO] [1703722424.357224667] [task_publisher]: Publishing: 'task 4'
[spin_some_repeat-2] [INFO] [1703722424.358609446] [task_subscriber]: task is complete task 3
[spin_some_repeat-2] [INFO] [1703722425.358905006] [subscriber-main-thread]: ----- call spin_some
[spin_some_repeat-2] [INFO] [1703722425.359112045] [task_subscriber]: start the task task 4
[spin_some_repeat-2] [INFO] [1703722426.359259373] [task_subscriber]: task is complete task 4
[spin_some_repeat-2] [INFO] [1703722426.359453598] [task_subscriber]: start the task task 4
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.
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.
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.
The output of the program is presented as follows:
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [publisher-1]: process started with pid [112108]
[INFO] [spin_until_future_complete-2]: process started with pid [112110]
[spin_until_future_complete-2] [INFO] [1703727953.315024507] [subscriber-main-thread]: ----- start to create the subscriber node
[publisher-1] [INFO] [1703727954.318600781] [task_publisher]: creating publisher and timer
[publisher-1] [INFO] [1703727956.320320568] [task_publisher]: Publishing: 'task 1'
[spin_until_future_complete-2] [INFO] [1703727958.320232632] [subscriber-main-thread]: ----- subscriber node is created
[publisher-1] [INFO] [1703727958.320275752] [task_publisher]: Publishing: 'task 2'
[spin_until_future_complete-2] [INFO] [1703727958.320647246] [subscriber-main-thread]: ----- start spin_some
[spin_until_future_complete-2] [INFO] [1703727958.320657151] [subscriber-main-thread]: doing some work [0]
[spin_until_future_complete-2] [INFO] [1703727958.320914234] [task_subscriber]: start the task task 2
[spin_until_future_complete-2] [INFO] [1703727959.321089551] [task_subscriber]: task is complete task 2
[spin_until_future_complete-2] [INFO] [1703727959.321303642] [task_subscriber]: start the task task 2
[publisher-1] [INFO] [1703727960.320299603] [task_publisher]: Publishing: 'task 3'
[spin_until_future_complete-2] [INFO] [1703727960.321402142] [task_subscriber]: task is complete task 2
[spin_until_future_complete-2] [INFO] [1703727960.321625114] [task_subscriber]: start the task task 3
[spin_until_future_complete-2] [INFO] [1703727961.321797323] [task_subscriber]: task is complete task 3
[spin_until_future_complete-2] [INFO] [1703727961.321989861] [task_subscriber]: start the task task 3
[publisher-1] [INFO] [1703727962.320294331] [task_publisher]: Publishing: 'task 4'
[spin_until_future_complete-2] [INFO] [1703727962.322159138] [task_subscriber]: task is complete task 3
[spin_until_future_complete-2] [INFO] [1703727962.322412436] [task_subscriber]: start the task task 4
[spin_until_future_complete-2] [INFO] [1703727963.320893505] [subscriber-main-thread]: doing some work [1]
[spin_until_future_complete-2] [INFO] [1703727963.322512534] [task_subscriber]: task is complete task 4
[spin_until_future_complete-2] [INFO] [1703727963.322717466] [task_subscriber]: start the task task 4
[publisher-1] [INFO] [1703727964.320283152] [task_publisher]: Publishing: 'task 5'
[spin_until_future_complete-2] [INFO] [1703727964.322817282] [task_subscriber]: task is complete task 4
[spin_until_future_complete-2] [INFO] [1703727964.323053314] [task_subscriber]: start the task task 5
[spin_until_future_complete-2] [INFO] [1703727965.323226421] [task_subscriber]: task is complete task 5
[spin_until_future_complete-2] [INFO] [1703727965.323427751] [task_subscriber]: start the task task 5
[publisher-1] [INFO] [1703727966.320294448] [task_publisher]: Publishing: 'task 6'
[spin_until_future_complete-2] [INFO] [1703727966.323529903] [task_subscriber]: task is complete task 5
[spin_until_future_complete-2] [INFO] [1703727966.323760117] [task_subscriber]: start the task task 6
[spin_until_future_complete-2] [INFO] [1703727967.323863154] [task_subscriber]: task is complete task 6
[spin_until_future_complete-2] [INFO] [1703727967.324076546] [task_subscriber]: start the task task 6
[publisher-1] [INFO] [1703727968.320294840] [task_publisher]: Publishing: 'task 7'
[spin_until_future_complete-2] [INFO] [1703727968.321037042] [subscriber-main-thread]: doing some work [2]
[spin_until_future_complete-2] [INFO] [1703727968.324188134] [task_subscriber]: task is complete task 6
[spin_until_future_complete-2] [INFO] [1703727968.324408263] [task_subscriber]: start the task task 7
[spin_until_future_complete-2] [INFO] [1703727969.324533568] [task_subscriber]: task is complete task 7
[spin_until_future_complete-2] [INFO] [1703727969.324728485] [task_subscriber]: start the task task 7
[publisher-1] [INFO] [1703727970.320293347] [task_publisher]: Publishing: 'task 8'
[spin_until_future_complete-2] [INFO] [1703727970.324830376] [task_subscriber]: task is complete task 7
[spin_until_future_complete-2] [INFO] [1703727970.325058938] [task_subscriber]: start the task task 8
[spin_until_future_complete-2] [INFO] [1703727971.325161432] [task_subscriber]: task is complete task 8
[spin_until_future_complete-2] [INFO] [1703727971.325351216] [task_subscriber]: start the task task 8
[publisher-1] [INFO] [1703727972.320277252] [task_publisher]: Publishing: 'task 9'
[spin_until_future_complete-2] [INFO] [1703727972.325452600] [task_subscriber]: task is complete task 8
[spin_until_future_complete-2] [INFO] [1703727972.325691728] [task_subscriber]: start the task task 9
[spin_until_future_complete-2] [INFO] [1703727973.321177505] [subscriber-main-thread]: ----- !!! ----- all the side word is done.
[spin_until_future_complete-2] [INFO] [1703727973.325863001] [task_subscriber]: task is complete task 9
[spin_until_future_complete-2] [INFO] [1703727973.326011324] [subscriber-main-thread]: ----- spin_some ends
[INFO] [spin_until_future_complete-2]: process has finished cleanly [pid 112110]
[publisher-1] [INFO] [1703727974.320284937] [task_publisher]: Publishing: 'task 10'
[publisher-1] [INFO] [1703727976.320347662] [task_publisher]: Publishing: 'task 11'
The key part in the output is highlighted in the section below:
[spin_until_future_complete-2] [INFO] [1703727972.325452600] [task_subscriber]: task is complete task 8
[spin_until_future_complete-2] [INFO] [1703727972.325691728] [task_subscriber]: start the task task 9
[spin_until_future_complete-2] [INFO] [1703727973.321177505] [subscriber-main-thread]: ----- !!! ----- all the side word is done.
[spin_until_future_complete-2] [INFO] [1703727973.325863001] [task_subscriber]: task is complete task 9
[spin_until_future_complete-2] [INFO] [1703727973.326011324] [subscriber-main-thread]: ----- spin_some ends
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.
In this experiment, we run a long-running task in a future and pass it to the spin_until_future_complete method. Due to , additional setup is required to wake up the executor, allowing the executor to exit from the spin_until_future_complete method.