回旋函数ros::spin()及ros::spinOnce()相关理解(结合实例)

回旋函数
/*
    ros::spinOnce()

    作用:处理一轮回调;在循环体内,处理所有可用的回调函数

*/
/*
    ros::spin()

    作用:进入循环处理回调

*/

相同点:二者都用于处理回调函数;

不同点:

这里先了解ros如何处理回调函数

ROS:回调函数处理与回调队列_ros 回调函数队列-CSDN博客

A.ros::spin()

当函数执行到ros::spin(),就不再继续向下执行,而是循环且监听消息队列

ros::spin() 是进入了循环监听消息队列,且执行对应回调函数,在 ros::spin() 后的语句不会执行到; ros::spin() 一般不会放在循环体里执行(会使循环失去意义),常放到程序的末尾执行。

B.ros::spinOnce()

ros::spinOnce() 只会执行一次回调函数,处理结束后继续执行ros::spinOnce()后的代码

个人理解:当每次执行到回调函数时,并不会立即调用并处理,而是将其放到一个回调函数的消息队列中,spin就是循环监听这个队列,等待新消息的到来,当队列中有了新的回调函数就处理,而spinonce是处理一次消息队列中的所有消息,然后就去执行其他代码。

示例:

# 发布方
// 1.包含头文件
#include "ros/ros.h"
#include "std_msgs/String.h" // ros中文本类型
#include <sstream>

int main(int argc, char *argv[])
{
    // 2.初始化ros节点
    ros::init(argc,argv,"pub");
    // 3.创建节点句柄
    ros::NodeHandle nh;
    // 4.创建发布者对象
    ros::Publisher pub = nh.advertise<std_msgs::String>("pos",10);
    // 5.编写发布逻辑并发布数据
    // 要求以10hz的频率发布数据,并且文本后添加编号
    // 先创建被发布消息
    std_msgs::String msg;
    // 发布频率
    ros::Rate rate(5); // 该参数为指定频率
    // 设置编号
    int count = 0;
    // 程序休眠,防止节点还未在roscore中注册时就发送消息
    ros::Duration(3).sleep();
    // 编写循环,循环中发布数据
    while(ros::ok())
    {
        // 输出中文
        setlocale(LC_ALL,"");
        count++;
        // 实现字符串拼接数字
        // msg.data = "here";
        std::stringstream ss;
        ss << "hello ---> " << count;
        msg.data = ss.str();
        pub.publish(msg);
        ROS_INFO("发布的数据是:%s",ss.str().c_str());
        // 根据前面制定的发送频率自动休眠 休眠时间 = 1/频率;
        rate.sleep();
    }
    return 0;
}
# 订阅方
// 1.包含头文件
#include "ros/ros.h"
#include "std_msgs/String.h" // ros中文本类型

void domsg1(const std_msgs::String::ConstPtr &msg)
{
    // 通过msg获取并操作订阅到的数据
    //ros::Duration(1);
    ROS_INFO("A接受到的数据是:%s",msg->data.c_str());
}

void domsg2(const std_msgs::String::ConstPtr &msg)
{
    // 通过msg获取并操作订阅到的数据
    ROS_INFO("B接受到的数据是:%s",msg->data.c_str());
}

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    // 2.初始化ros节点
    ros::init(argc,argv,"sub");
    // 3.创建节点句柄
    ros::NodeHandle nh;
    // 4.创建订阅者对象
    ros::Subscriber sub1 = nh.subscribe("pos",10,domsg1);
    ros::Subscriber sub2 = nh.subscribe("pos",10,domsg2);
    int count_tmp = 0;
    while(ros::ok()){
        count_tmp++;
        ros::Duration(1).sleep();
        ROS_INFO("这是第%d次接受数据。",count_tmp);
        ros::spinOnce();
    }
   
    // 5.处理订阅到的数据
    // 6.循环读取接收的数据,并调用回调函数处理
    // 如果要用回调函数必须用spin
    
    ROS_INFO("I'm here!");
    return 0;
}

运行结果:

image-20240327205604136

分析:

发布方的发布频率是每秒5个包,订阅方每1秒执行一次ros::spinOnce(),此时消息队列里面存放了过去1s两个订阅方收到的2*5条消息(除第1次,可能发布方发布消息时订阅方已经开始计时),程序会处理当时队列中存在的所有回调函数,处理完毕后继续向下执行。

理解过程中遇到的问题

发布方代码如上,订阅方代码刚开始如下:

#include "ros/ros.h"
#include "std_msgs/String.h" // ros中文本类型

void domsg1(const std_msgs::String::ConstPtr &msg)
{
    //ros::Duration(1);
    ROS_INFO("A接受到的数据是:%s",msg->data.c_str());
}

void domsg2(const std_msgs::String::ConstPtr &msg)
{
    ROS_INFO("B接受到的数据是:%s",msg->data.c_str());
}

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"sub");
    ros::NodeHandle nh;
  
    ros::Subscriber sub1 = nh.subscribe("pos",10,domsg1);
    ros::Subscriber sub2 = nh.subscribe("pos",10,domsg2);
    //!!!注意这里注释了延时函数
    //ros::Duration(1).sleep();
  
    ros::spinOnce();
    ROS_INFO("I'm here!");
    return 0;
}

运行结果:

image-20240327210528982

在这种状态下多次执行订阅方代码并没有订阅到数据,预期应该是会执行一次回调函数,所以一直不理解ros::spinOnce()的工作原理(其实这里应该是对消息订阅的工作模式没有理解)

此时,解开这句注释 ros::Duration(1).sleep();

再次执行:

image-20240327210810673

分析:

在执行以下两行代码时,应该是打开对于话题"pos"的订阅状态,如果不延时,程序很快(执行一两行代码的时间)就跑到ros::spinOnce(),而发布方的发布频率是每秒5个包,在这么短时间,确实没有订阅到话题"pos"的相关消息,导致相关回调函数也没有得到执行;而在代码后延时1s,在这1s过程中,就可以接收到"pos"话题相关的消息,从而使回调函数得到执行。

ros::Subscriber sub1 = nh.subscribe("pos",10,domsg1);
ros::Subscriber sub2 = nh.subscribe("pos",10,domsg2);

这里确实花了点心思去理解回旋函数,希望一点记录可以帮到同样有困惑的朋友,特此单发一条

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐