ROS:回调函数处理与消息队列
参考资料:
在ROS节点的执行过程中,如果程序中存在订阅器和回调函数,就必然会面临两个问题:
- 订阅器的队列长度选择多大?
- 该使用
spinOnce()
还是spin()
处理回调函数?
1.话题回调机制
我在查找相关资料时得知,ROS在处理回调函数时,并不是消息传来就立刻进行处理的,特别是在使用spinOnce()
处理回调函数时。即便使用spin()
处理时,若发布器的发布频率过快,同样会导致无法及时处理回调函数的问题。此时,就需要用到我们在定义订阅器时的队列长度了。
当节点无法及时处理回调函数时,这些未被处理的回调函数就会按照先后的顺序放入一个消息队列,该回调队列的长度就是定义订阅器时的队列长度了。节点会先处理时间辍最小的回调函数,然后依次处理队列中所有的回调函数。但是,当发布器的频率过快时,会出现未被处理的回调函数数量超过队列长度,它会自动丢弃时间辍最长(最老的)的回调函数。那么,如果我们只想要处理当前最新的消息时,只需要把发布器和订阅器的队列长度设置为1即可。
spinOnce()
和spin()
的区别这里就不再赘述,需要注意的是,spinOnce()
在执行的那一刻,并不是只处理队列中的一个回调函数就返回了,而是会处理当时队列中存在的所有回调函数。
ros::spinOnce()
will call all the callbacks waiting to be called at that point in time.
ros::Rate r(10); // 10 hz
while (should_continue)
{
... do some work, publish some messages, etc. ...
ros::spinOnce();
r.sleep();
}
下图是使用spinOnce()
进行回调函数处理,在回调函数中打印"turtle pose",在主循环中打印"counter"。从该图可以发现,spinOnce()
处理了多个回调函数才返回主循环。这也证明了上述的论断。
官方文档中,spin()
和spinOnce()
可以等价于以下程序:
// spin()
#include <ros/callback_queue.h>
ros::NodeHandle n;
while (ros::ok())
{
ros::getGlobalCallbackQueue()->callAvailable(ros::WallDuration(0.1));
}
// spinOnce()
#include <ros/callback_queue.h>
ros::getGlobalCallbackQueue()->callAvailable(ros::WallDuration(0));
函数callAvailable()
说明会处理队列中所有可用的消息。ros::WallDuration(0)
则表示只处理在当时时刻的队列消息,不会等待,若当时无任何消息,则直接返回。
还有一个函数callOne()
也会处理队列中的消息,但它只会处理队列中时间辍最长的那一个消息。
The
CallbackQueue
class has two ways of invoking the callbacks inside it:callAvailable()
andcallOne()
.callAvailable()
will take everything currently in the queue and invoke all of them.callOne()
will simply invoke the oldest callback on the queue.Both
callAvailable()
andcallOne()
can take in an optional timeout, which is the amount of time they will wait for a callback to become available before returning. If this is zero and there are no callbacks in the queue the method will return immediately.
2.多话题回调机制
当节点中只有一个订阅器和对应回调函数时,整个过程都很清晰。但如果节点中存在多个话题订阅器和多个回调函数时,整个过程又是什么样的呢?
spin()
andspinOnce()
are really meant for single-threaded applications, and are not optimized for being called from multiple threads at once.
在官方文档中写的十分清楚,spin()
和spinOnce()
只是单线程函数,并不适用于多线程处理。也就是说,只要使用了这两个函数来处理回调函数,那么程序必然是单线程的。
在单话题回调时,必然只存在一个消息队列。但若是多话题回调,又该如何呢?在这里,我就想到了一些问题:
- 多个话题的订阅器是否共享一个消息队列?;
- 若它们共享一个消息队列,那为什么每个订阅器在初始化时都可以设置队列长度?
- 是否可以自定义设置多个消息队列?
2.1.问题一:多个话题的订阅器是否共享一个消息队列?
关于第一个问题,官方文档中有说明。在默认情况下,所有的回调函数都会被分配到一个全局队列中,然后由spin()
或其它函数进行处理。也就是说,多话题的订阅的消息是共享一个全局消息队列的。
By default, all callbacks get assigned into that global queue, which is then processed by
ros::spin()
or one of the alternatives.
那么,根据上文spinOnce()
的描述,在执行spinOnce()
时会处理当时在全局队列中所有的消息。若当时队列中存在多个话题消息,那么就会处理多个话题的回调函数。但由于是单线程的,因此处理的过程是串行的。如这里所描述的。
2.2.问题二:若它们共享一个消息队列,那为什么每个订阅器在初始化时都可以设置队列长度?
关于第二个问题,我还没有在官方文档中找到相关描述。
但我猜想,各个订阅器设置的队列长度是指该订阅话题在整个全局队列中所占的长度。也就是说,全局队列的长度至少不会小于所有话题订阅器设置的队列之和。
2.3.问题三:是否可以自定义设置多个消息队列?
可以自定义设置消息队列,roscpp
中提供了相应函数和类。详情可参照官方文档。