目录
写在前面
1. ROS里的spin和spinOnce
1.1 回调机制浅析
1.2 为什么订阅话题时要指定queue_size?
1.3 设置queue_size的小技巧
1.4 spin和spinOnce用法总结:
2. ROS2里的spin_some和spin
2.1 揣摩一下spin和spin_some的官方注释
2.2 spin_some的一点小不同
最后的话
写在前面
ROS2有spin_some, spin,而ROS有spinOnce,spin,他们有什么区别和联系呢?
如果你学过ROS,那么只用看第一部分。
如果你直接学ROS2,也建议按顺序看,加深理解。
1. ROS里的spin和spinOnce
如果你刚接触ROS,很可能看过这份很详细的ROS官方教程,它提到spin和spinOnce的基本用法。但是,我估计,极大可能,看完你还是不明白两者有什么区别,又该如何去用。
ROS/Tutorials/WritingPublisherSubscriber(c++) - ROS Wikihttp://wiki.ros.org/ROS/Tutorials/WritingPublisherSubscriber%28c++%29别着急,我们把相关内容提取出来,品一品。
ros::spinOnce()
Calling ros::spinOnce() here is not necessary for this simple program, because we are not receiving any callbacks. However, if you were to add a subscription into this application, and did not have ros::spinOnce() here, your callbacks would never get called. So, add it for good measure.
上面这段话,摘自publisher的节点代码注释。
大意是,作为一个纯粹的、简单的publisher程序,不需要使用spinOnce(),因为它不执行任何回调。但是,如果想在这个程序里增加订阅功能,而不使用spinOnce(), 回调将不会产生。
ros::spin()
ros::spin() enters a loop, calling message callbacks as fast as possible. Don't worry though, if there's nothing for it to do it won't use much CPU. ros::spin() will exit once ros::ok() returns false, which means ros::shutdown() has been called, either by the default Ctrl-C handler, the master telling us to shutdown, or it being called manually.
这段话,摘自subscriber的节点代码注释。
大意是,spin()进入一个无限循环,会尽可能快地执行消息的回调。但是,不需要担心CPU占用问题,因为没事干的时候,它并不怎么消耗CPU资源。有好几个方法可以触发spin()的退出,比如ros:ok()返回false(通常来自ros::shutdown()调用结果,可来自ctrl-c句柄,或手动调用)。
1.1 回调机制浅析
我们大概捋一捋背后的原理。
首先,只有使用了回调函数的node才需要使用spin或spinOnce。通常是需要订阅topic的node。
但是,消息订阅器Subcriber只是为topic指定了callback函数。当程序接收到该topic后,并没有立即执行callback函数,而是把callback函数放到了一个回调函数队列中。我们可以认为,每收到一个topic,就会将相应的callback函数进入队列中,它们函数名相同,只是实参不同。
那么,什么时候才会执行回调函数队列里的callback函数呢?
这就要借助ros::spin()和ros::spinOnce()了。
当spinOnce函数被调用时,系统会处理回调函数队列队首的callback函数,执行完后退出。所以,这会有一个问题。由于任何回调函数队列的长度是有限的,如果发布器发送数据的速度太快,队列里的旧数据会被覆盖掉。当spinOnce函数调用的频率太低,就会导致数据的丢失。
而spin函数,也可以调用回调函数队列里的callback函数。与spinOnce不同的是,即便回调函数队列为空,它不会退出,而是循环地等待回调函数队列里有新任务。一旦队列有了callback函数,它就会马上去执行。如果没有的话,它就会继续阻塞。
是的,spin()会导致node进入阻塞状态,但是别担心,它占用不了多少CPU资源。
1.2 为什么订阅话题时要指定queue_size?
在订阅某个话题时,会指定queue_size参数。它是回调函数列表的长度。订阅话题时,有订阅缓存区;发布话题时,有发布缓存区,他们限制了缓存队列的长度。
那么,为什么有这个参数呢?
对于某个订阅的话题,如果发布频率很快,而回调函数中处理时间很长,或者因为spinOnce执行的频率过低,那么,在这段处理的时间窗口里,会收到一些已订阅的话题内容,这些topic内容又会触发产生相应的回调任务,这些回调任务来不及处理,只能进入队列,也就是订阅缓冲区。
等到执行spin函数,或再次执行spinOnce时,系统会再次处理队列中的回调任务。
如果缓存区足够大,偶尔回调函数执行超时,还可以在缓冲区内保存历史话题信息,确保信息不会丢失。但是,如果缓存区不够大,或者回调函数处理时间持续超时,那么,免不了会出现缓冲区溢出。此时,缓存区中最久远的回调任务将丢失,新的回调任务补充进来。
也就是说,缓冲区是个FIFO先进先出的队列。
1.3 设置queue_size的小技巧
通常,如果对实时性要求高,想每次处理最新的发布信息,那么,queue_size可以设置为1,这样每一次的回调处理的都是最新的话题。
如果queue_size是0,则表明回调函数队列是无穷大。如果不想错过所有发布的话题,可以将queue_size设置的稍微大一点,相应地也占用更多的资源。
ros::spinOnce()执行频率是5Hz,而所订阅的话题频率是10Hz,通俗地说,两次执行spinOnce的间隔时间内,话题发布了两次,那么,很明显,缓冲区的队列长度一定要大于2,才又可能保证数据不丢失。
当然,这样依然没法确保信息不丢失,只能说是“有可能保证不丢失数据”。因为回调函数可能执行的时间很长等等。这就是另外一个问题,不在本文总结的范围内。
1.4 spin和spinOnce用法总结:
1)千万不要认为,只要指定了回调函数,系统就会自动触发它。只有当程序执行到了ros::spin()
或者ros::spinOnce(),
才能真正使回调函数生效。
2)程序执行到ros::spin()之后,会一直去话题订阅缓冲区中查看有没有回调函数。如果有,则处理回调函数;否则,继续查看并且等待。所以,当程序执行了ros::spin()之后,就会持续等待回调函数,不处理其他任务。也就是说,在ros::spin()后面的代码没机会执行了。
3)当程序执行到ros::spinonce()时,会去话题订阅缓冲区中查看有没有回调函数。如果有,就马上处理一个回调函数并退出;否则,就退出这个指令,执行后续的代码。
4) ros::spinOnce比ros::spin()更加灵活,常用于手动写循环,可以放在程序的任何位置下,但是需要考虑spinOnce执行频率与所订阅话题的发布频率之间的关系。而spin()就比较粗暴,反正一直在等话题产生,只要注意回调函数不持续超时,或者确保订阅缓冲区足够大就好。
--------------------------------------------------------------------
2. ROS2里的spin_some和spin
ros2中并没有spinOnce,取而代之的时spin_some。
我们可以简单理解为,ROS1和ROS2的spin()函数用法相同,spin_some()与ROS1 ros::spinOnce()用法相同。
2.1 揣摩一下spin和spin_some的官方注释
spin()
Create a default single-threaded executor and spin the specified node.
Do work periodically as it becomes available to us. Blocking call, may block indefinitely.
spin()创建一个默认的单线程执行器,服务于指定的节点。它生效后,周期运行。无限阻塞状态。
spin_some()
Create a default single-threaded executor and execute any immediately available work.
Complete all available queued work without blocking.This function can be overridden. The default implementation is suitable for a single-threaded model of execution. Adding subscriptions, timers, services, etc. with blocking callbacks will cause this function to block (which may have unintended consequences).
spin()创建一个默认的单线程执行器,立即处理有效的回调任务,处理队列里所有的任务,但不阻塞。从截至到这里的描述来看,确实与ROS1的spinOnce功能相似。
然后,第二段说,这个功能可以重写!它的默认实现适用于执行单线程。当增加订阅的话题、计时器、服务以及阻塞式回调功能后,可能会导致这个“原本非阻塞”的功能也阻塞起来,并带来不可预知的后果。
这也没啥,我们知道这些就好了。
就我的理解,通俗地说,spin_some和spinOnce最大的不同是,前者一旦开始干活,要尽可能地去处理订阅缓冲区内的有效回调;而后者spinOnce开始干活后,只处理队列最前面的一个回调。
2.2 spin_some的一点小不同
使用spin_some有些小细节要注意了,以C++为例。
spin_some后带有参数node,该参数类型为rclcpp::Node类型的智能指针,而在使用spinOnce时,其 this指针指向的是我们自定义的某个类,即rclcpp::Node的一个子类,因此,直接带入this指针,rclcpp::spin_some(this)会报类型不匹配。
正确的用法是,在上下文新创建一个rclcpp::Node的智能指针,并指向其子类对象this,即
rclcpp::Node::SharedPtr node_(this); // 创建基类指针,指向子类对象this
rclcpp::spin_some(node_); // 运行正常
最后的话
所谓阻塞,就是函数一旦进入角色,就困在自己的世界出不来。比如spin。
所谓非阻塞,就是该干活的时候干活,该休息的时候休息。每次干活只干一件,那是spinOnce;一旦有机会干,可以多干,那是spin_some。
就到这里。
希望对你有所帮助。