一文搞懂ROS2的spin_some, spin和ROS的spinOnce

目录

写在前面

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 Wiki

http://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。

就到这里。

希望对你有所帮助。

原文转载:https://blog.csdn.net/slampai/article/details/127992755

 如过冒犯,请联系删除!

  • 8
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值