【ROS】的单线程Spinning和多线程Spinning

参考:

  1. https://www.cnblogs.com/feixiao5566/p/5288206.html
  2. https://www.freesion.com/article/9499126134/
  3. https://blog.csdn.net/yaked/article/details/50776224

单线程Spinning

  • 单线程回调函数 ros::spin() 与 ros::spinOnce()

    这两个函数的学名叫ROS消息回调处理函数。它俩通常会出现在ROS的主循环中,程序需要不断调用ros::spin() 或 ros::spinOnce(),两者区别在于前者调用后不会再返回,也就是你的主程序到这儿就不往下执行了,而后者在调用后还可以继续执行之后的程序

    关于消息接收回调机制在ROS官网上略有说明 (callbacks and spinning)。总体来说其原理是这样的:除了用户的主程序以外,ROS的socket连接控制进程会在后台接收订阅的消息,所有接收到的消息并不是立即处理,而是等到spin()或者spinOnce()执行时才集中处理。所以为了保证消息可以正常接收,需要尤其注意spinOnce()函数的使用 (对于spin()来说则不涉及太多的人为因素)。

1. ros::spin()

  • ros::spin()是最简单的单线程自旋, 它会一直调用直到结束。

    用法: ros::spin()

#include "ros/ros.h"
#include "std_msgs/String.h"
 
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
    ROS_INFO("I heard: [%s]", msg->data.c_str());
}
  
int main(int argc, char **argv)
{
    ros::init(argc, argv, "listener");
    ros::NodeHandle n;
    ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);
    /**
     * ros::spin() 将会进入循环, 一直调用回调函数chatterCallback(),每次调用1000个数据。
     * 当用户输入Ctrl+C或者ROS主进程关闭时退出,
     */
    ros::spin(); 
    return 0;
}
  • 我们自己实现一个简单的与ros::spin()用法相同的例子:
ros::getGlobalCallbackQueue()->callAvailable(ros::WallDuration(0.1));

2. ros::spinOnce()

  • ros::spinOnce()定期调用等待在那个点上的所有回调;
  • 对于ros::spinOnce()的使用,虽说比ros::spin()更自由,可以出现在程序的各个部位,但是需要注意的因素也更多。比如:如果对于用户自己的周期性任务,最好和spinOnce()并列调用。即使该任务是周期性的对于数据进行处理,例如对接收到的IMU数据进行Kalman滤波,也不建议直接放在回调函数中:因为存在通信接收的不确定性,不能保证该回调执行在时间上的稳定性。

;对于有些传输特别快的消息,尤其需要注意合理控制消息池大小和ros::spinOnce()执行频率。比如消息送达频率为10Hz, ros::spinOnce()的调用频率为5Hz,那么消息池的大小就一定要大于2,才能保证数据不丢失,无延迟。

用法:ros::spinOnce()

#include "ros/ros.h"
#include "std_msgs/String.h"
  
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
    /*...TODO...*/ 
}
  
int main(int argc, char **argv)
{
    ros::init(argc, argv, "listener");
    ros::NodeHandle n;
    ros::Subscriber sub = n.subscribe("chatter", 2, chatterCallback);
  
    ros::Rate loop_rate(5);
    while (ros::ok())
    {
        /*...TODO...*/ 

        ros::spinOnce();  // 只调用一次, 必须放在一个 while循环中
        loop_rate.sleep();
    }
    return 0;
}
  • 我们自己实现一个简单的与ros::spinonce()用法相同的例子:
ros::getGlobalCallbackQueue()->callAvailable(ros::WallDuration(0));

3. ros::spin()和ros::spinOnce()

  • ros::spin() 在调用后不会再返回,也就是你的主程序到这儿就不往下执行了,而 ros::spinOnce() 后者在调用后还可以继续执行之后的程序。在使用ros::spin()的情况下,一般来说在初始化时已经设置好所有消息的回调,并且不需要其他背景程序运行。这样以来,每次消息到达时会执行用户的回调函数进行操作,相当于程序是消息事件驱动的;而在使用ros::spinOnce()的情况下,一般来说仅仅使用回调不足以完成任务,还需要其他辅助程序的执行:比如定时任务、数据处理、用户界面等。

    其实看函数名也能理解个差不多,一个是一直调用;另一个是只调用一次,如果还想再调用,就需要加上循环了。

    这里一定要记住,ros::spin()函数一般不会出现在循环中,因为程序执行到spin()后就不调用其他语句了,也就是说该循环没有任何意义,还有就是spin()函数后面一定不能有其他语句(return 0 除外),有也是白搭,不会执行的。ros::spinOnce()的用法相对来说很灵活,但往往需要考虑调用消息的时机,调用频率,以及消息池的大小,这些都要根据现实情况协调好,不然会造成数据丢包或者延迟的错误。

  • 以上是它们的基础用法,那么spin到底做了什么呢?

    首先, 当我们调用ros::spin时, 会有一个互斥锁, 把你的回调队列加锁, 防止执行混乱;

    然后, 检测如果回调队列不为空, 则读取回调队列;

    最后,当while(nh.ok())为true时, 调用当前队列中的所有函数,如果有不满足的,会重新放回队列中。

    所以listener中, 就一直执行着ros::spin来监听话题了。

    从这样看来,spin和spinOnce的区别之一,就是while(nh::ok())执行块的大小了。另一个是等待时间,spin在执行时, 会指定一个返回前可以等待调用的时间。spin会等待0.1s,而spinonce不会。

  • 什么时候用ros::spin()和ros::spinOnce()呢?

    如果仅仅只是响应topic,就用ros::spin()。当程序中除了响应回调函数还有其他重复性工作的时候,那就在循环中做那些工作,然后调用ros::spinOnce()。

    1. ros::spin():下面的打印输出不会更新,相当于只执行了一次,但是它会不断处理ROS 的message queue(下图右边的subscriber 开头的打印函数只调用了一次,然后一直响应订阅的消息)。
      【ROS】的单线程Spinning和多线程Spinning_第1张图片
    2. ros::spinOnce():一直在打印 while 中的部分,循环调用print( )函数,并且处理message queue。
      【ROS】的单线程Spinning和多线程Spinning_第2张图片
      spinOnce( )较常用的做法是while里放publisher所要发布的msg的赋值处理,然后一直循环发布topic。
ros::Rate loop_rate(10);
 
  while (ros::ok())
  {
    std_msgs::String msg;
 
    std::stringstream ss;
    ss << "hello world " << count;
    msg.data = ss.str();
 
    chatter_pub.publish(msg);
 
    ros::spinOnce();
    loop_rate.sleep();
  }
  • spinOnce使得pub/sub为非阻塞锁;spin是客户端的, 因此是阻塞的

    这样就很好理解talker要用SpinOnce。有需要talk的时候发出,没有的时候不发送。而listener一直在阻塞着听。

  • 这样,再来说之前很流传的一句关于解释spin的话,“所有的回调函数都是spin调用的”。这是一句形象而不准确的话。回调函数一直等待在回调队列中, 只要条件一满足就会发生回调, 而spin的作用, 只是创建了线程给这个回调函数去执行它, 这样多线程就不会影响其他的作业。

    之所以用spin, 是因为rospy不愿指定线程模型, 在程序中将线程暴露出来, 而用spin来把它封装起来. 但你可以用多线程调用任意数量的回调函数.

    没有用户订阅, 服务和回调是不会被调用的.

多线程Spinning

  • 防阻塞多线程回调函数 ros::MultiThreadedSpinner 和 ros::AsyncSpinner

    对于一些只订阅一个话题的简单节点来说,我们使用ros::spin()进入接收循环,每当有订阅的话题发布时,进入回调函数接收和处理消息数据。但是更多的时候,一个节点往往要接收和处理不同来源的数据,并且这些数据的产生频率也各不相同,当我们在一个回调函数里耗费太多时间时,会导致其他回调函数被阻塞,导致数据丢失。这种场合需要给一个节点开辟多个线程,保证数据流的畅通

  • 为了观察不同话题的消息被阻塞的情况,可以参考以下实验代码:

  1. 多topic发布:https://github.com/GuoXiaoxiao1/wlh_ros_demo/blob/master/multi_thread_demo/src/multi_topic_pub.cpp

  2. 多topic接收:https://github.com/GuoXiaoxiao1/wlh_ros_demo/blob/master/multi_thread_demo/src/multi_topic_sub.cpp

    可以看到,发布程序中,以10hz的频率发布了chatter1和chatter2两个话题;在订阅程序中,回调函数1中加入了2s的延时,导致了回调函数2也只能2s才能接收到一个数据。为了使回调函数2能正常接收数据,使用在一个ROS节点中开辟多个线程的方法。

  • 在ROS中,有两种方法可以在一个节点中开辟多个线程:

1. ros::MultiThreadedSpinner

  • MultiThreadedSpinner是阻塞式的spinner,类似ros::spin()。

  • 在构造过程中可以指定它所用线程数,但如果不指定线程数或者线程数设置为0,它将在每个cpu内核开辟一个线程。

    用法如下:

ros::MultiThreadedSpinnerspinner(4); // Use 4 threads

spinner.spin();// spin() will not return until the node has been shutdown

2. ros::AsyncSpinner

  • ros::AsyncSpinner (since 0.10):很有用的线程spinner是AsyncSpinner,它不是阻塞式的spin()调用;

  • AsyncSpinner比MultiThreadedSpinner更优,它有start() 和stop() 函数,并且在销毁的时候会自动停止。

    下面的用法等价于上面的MultiThreadedSpinner例子:

ros::AsyncSpinner spinner(4); // Use4 threads

spinner.start();

ros::waitForShutdown();

你可能感兴趣的:(多线程,c++,ros)