话题、服务、动作通信方式的对比
话题通信:异步通信
话题通信的好处在于:只要双方通过同一个topic建立联系,talker就可以向topic一直发送数据,listener也可以从topic中实时的获取数据。只要这个联系一直存在,那么数据的单向传输就不会中断。
那现在我们试想一下这样的情况:我想要控制机械臂一直在空中旋转并且实时的获取机械臂的运动位姿,那作为单向传输的话题通信就有些复杂了:
要用到两个topic才可以实现,这样一看确实有些复杂。
服务通信:同步通信
服务通信是一种“同步通信”,其优点就在于:无论何时只要客户端发送请求服务器一定给予实时响应。但是这种实时响应的缺点在于“only one”,也就是说服务通信双方一旦建立联系仅仅通信一次就立刻解裂。好处在于:相较于话题通信而言,服务通信有了反馈信息,我们可以通过服务端回传的response分析此次响应执行的情况。但最大的缺点在于response信息仅仅在响应完毕才给予回传,在任务执行过程中服务端仍不会回传任何反馈信息。
那再让我们想象一下:如果我的request是“小车按照指定路线行驶1km”,我们只有小车按照指定路线行驶1km到达目的地之后,服务端才会回传信息。在小车运动过程中,我们并不知道小车的任何信息。因此,服务通信的缺点在于它的实时性还不够:
动作通信
我们继续一种满足以下优势的通信模式:
- 可以实时反馈信息以及通信的状态
- 双方建立的通信关系在任务执行过程中稳定存在,并不需要一直存在
- 当出现意外情况时,我们可以先打断该通信联系,优先执行另一个通信
- 我们可以有条件地打断该通信联系
- 我们需要发送请求并且再执行完毕后知道响应结果
动作通信就可以完成这些要求。动作通信双方的逻辑关系如下:
其中,goal,cancel,feedback,state,result的含义如下:
- goal:客户端发送的请求,也就是客户端要求服务端做到的事情
- cancel:打断双方的通信联系,打断分两种:一种是两者的通信真正地被切断,另一种是两者的通信被意外的事件所打断,等处理完意外的事件之后,两者之间的通信自然恢复
- feedback:实时反馈的信息
- state:通信的状态(通信正在进行、通信已经完成、通信被挂起、通信被打断……)
- result:客户端最终执行的结果
我们可以整个通信拆解为两部分分别进行形象的比喻:
① 客户端-->服务端
② 服务端-->客户端
其中,state表征着通信链路的状态分为以下几种:
| 通信状态 | 含义 | |
| Pending | 中间状态 | 目标服务器尚未处理目标 |
| active | 目标当前正在由操作服务器处理 | |
| recalled | 最终状态 | 目标在开始执行前收到取消请求,并已成功取消 |
| rejected | 目标被操作服务器拒绝,因为目标无法实现或无效 | |
| preempted | 目标在开始执行后收到取消请求,并已完成执行 | |
| aborted | 由于某些故障,操作服务器在执行过程中中止了目标 | |
| succeeded | 动作服务器成功地实现了目标 | |
| lost | 行动客户端可以确定目标已丢失,由客户端自己判断 | |
注意:这些变量都是使用枚举变量依次列举出来的,分别使用数字0-7表示。
我的ROS项目文件的结构
动作通信的自定义文件格式
# goal
uint32 targeted_washed_dishes
---
# result
uint32 total_washed_dished
---
# feedback
uint32 now_number_washed_dished
关于请求响应的信息一共有三个,其先后顺序依次为:goal 、result、feedback。在配置完Package.xml和CMakelist.txt(无需配置“关于源文件编译链接的声明”)文件之后,我们可以开始Crtl+Shift+B开始编译.action文件,编译完成后我们在/devel/share/对应的package名称(actionProject)/msg下找到如下内容:
其实我们不难发现,action通信模式就是建立在.msg的基础之上,由于.msg话题通信可以实时的回传和发布信息,因此action通信是基于.msg而非基于.srv建立的:
由于客户端没有向服务器发送cancel信息,因此两者之间的通信大致可以如下图所示:
“用什么编译”以及“怎么编译”
Package.xml配置——用什么编译
catkin
actionlib
actionlib_msgs
roscpp
std_msgs
actionlib
actionlib_msgs
roscpp
std_msgs
actionlib
actionlib_msgs
roscpp
std_msgs
catkin actionlib actionlib_msgs roscpp std_msgs actionlib actionlib_msgs roscpp std_msgs actionlib actionlib_msgs roscpp std_msgs
CMakelist.txt配置——如何编译
① 声明编译依赖项
find_package(catkin REQUIRED COMPONENTS actionlib actionlib_msgs roscpp std_msgs )
② 声明功能包中自定义.action文件
add_action_files( FILES wash.action )
③ 声明“用于编译.action文件”的功能包
generate_messages( DEPENDENCIES actionlib_msgs std_msgs )
④ 声明“生成CMake文件”所需的功能包
catkin_package( # INCLUDE_DIRS include # LIBRARIES actionProject CATKIN_DEPENDS actionlib actionlib_msgs roscpp std_msgs # DEPENDS system_lib )
⑤ 关于源文件编译链接的声明
// 声明“需要编译成可执行文件”的源文件
add_executable(actionClient src/actionClient.cpp)
add_executable(actionServer src/actionServer.cpp)
// 声明“单个源文件编译后需要连接库文件”的源文件
target_link_libraries(actionClient
${catkin_LIBRARIES}
)
target_link_libraries(actionServer
${catkin_LIBRARIES}
)
客户端.cpp源文件的构建
基本流程
① 初始化节点:
ros::init(argc,argv,"actionClient");
② 在master主节点注册信息:
// define ActionClient-type object ros::NodeHandle nh; actionlib::SimpleActionClient obj(nh,"chatter",true); // true代表按照官方的方法管理线程,false代表自己手动管理线程
我们可以仔细地看一下“actionlib::SimpleActionClient”的构造函数:
SimpleActionClient(ros::NodeHandle & n, const std::string & name, bool spin_thread = true)
: cur_simple_state_(SimpleGoalState::PENDING)
{
initSimpleClient(n, name, spin_thread);
}
其中参数的含义如下:
1. ros::NodeHandle & n:用于指定函数句柄,这个参数可以省略,因为如果你不指定系统会给设定默认的节点句柄。该句柄的作用是向master主节点上报本node的信息;
2. const std::string & name:用于指定client的topic,因为action通信是基于msg话题通信实现的,因此通信双方需要一个topic使得通信双方可以建立通信联系;
3. bool spin_thread = true:表征着构造函数中是否需要使用额外线程。注意参数spin_thread的设置。如果spin_thread为false则需要自行开启线程。
如果设置为false,请参考如下博文:
ROS actionlib学习(三) - XXX已失联 - 博客园 (cnblogs.com)https://www.cnblogs.com/21207-iHome/p/8304658.html
③ client等待server开启之后,在发送消息:
// wait until the server appears
ROS_INFO("waiting for action server to start.");
obj.waitForServer();
ROS_INFO("Action server started, sending goal.");
这是为了避免:当客户端启动时服务端未启动,这导致客户端认为服务端不存在,最终导致客户端放弃建立通信关系。
④ 打包“client-->server发送的goal请求数据”:
// pack data actionProject::washGoal goal; goal.targeted_washed_dishes = 10;
我们这里设置的是:洗碗目标为10个。
⑤ client-->server提交请求,并且设置回调函数
// submit data to ActionServer obj.sendGoal(goal,&SimpleDoneCallbackFunc,&SimpleActiveCallbackFunc,&SimpleFeedbackCallbackFunc);
1. SimpleDoneCallbackFunc:收到result时的回调函数
void SimpleDoneCallbackFunc(const actionlib::SimpleClientGoalState& state,const actionProject::washResultConstPtr& result)
{
ROS_INFO("finish!nt");
ROS_INFO("Now State:%dnt",state.state_);
ROS_INFO("total_washed_dished:%dnt",result->total_washed_dished);
ros::shutdown(); // shut down the operation of node explicitly
}
2. SimpleActiveCallbackFunc:通信建立时的回调函数
void SimpleActiveCallbackFunc()
{
ROS_INFO("the node is active!nt");
}
3. SimpleFeedbackCallbackFunc:收到feedback反馈信息时的回调函数
void SimpleFeedbackCallbackFunc(const actionProject::washFeedbackConstPtr& feedback)
{
ROS_INFO("now_number_washed_dished:%dnt",feedback->now_number_washed_dished);
}
其实最难的一点就在于:我们不太容易获知函数参数的具体形式,我们可以参照函数原型进行确定:
函数模板: templatevoid SimpleActionClient::sendGoal(const Goal & goal, SimpleDoneCallback done_cb, SimpleActiveCallback active_cb, SimpleFeedbackCallback feedback_cb) 函数参数——函数模板: typedef boost::function SimpleDoneCallback; typedef boost::function SimpleActiveCallback; typedef boost::function SimpleFeedbackCallback;
⑥ 启动client:
obj.start();
void start()子函数用于启动client,当调用此函数时,client开始于server寻求建立通信联系。
⑦ 不断轮询以便实时接收server传回的信息:
// loop continueously to receive information ros::spin(); // when finishing goal, shutdown() will end the node
但是有个疑问:我们如何在server完成任务之后终止client的轮询状态呢?
这就需要我们注意:我们在使用setGoal函数时,我们设置了SimpleDoneCallbackFunc函数,这个函数用于“处理client接收到的来自server的result信息”。我们在该函数中加入了“void shutdown()”,这个函数的作用就是用于结束spin()的轮询工作。
附加知识
我们需要说明一下编写客户端.cpp源文件所需要用到的功能包:
#include "actionlib/server/ simple_action_client.h" #include "actionProject/washAction.h"
① “数据打包”工具包
首先,我们要重点介绍一下“actionProject/washAction”功能包,这个功能包的功能主要是:打包数据。前面介绍过,client-->server发送的数据一共有两个:goal和cancel,但其中,cancel作为由client发出的通信中止信号,无需变量承载,只需要调用相关函数即可实现,因此我们只需要打包goal——client-->server的请求数据即可。因此,我们只要给变量赋值即可完成数据打包操作:
// pack data actionProject::washGoal goal; goal.targeted_washed_dishes = 10;
这里,client和server双方双向通信需要打包的变量一共有三个:feedback,result,goal:
actionProject::washFeedback feedback; actionProject::washGoal goal; actionProject::washResult result;
这个功能包是我们.action自定义文件生成的,唯一的有用之处就在于打包数据,并且用得到的所有变量就是上面这三个。
② “通信”工具包
“actionlib/client/simple_action_client”工具包的用处在于“将打包好的数据信息client-->server进行传输”以及“接收server-->client回传的数据信息”。其成员函数如下:
1. 构造函数:
SimpleActionClient (ros::NodeHandle &n, const std::string &name, bool spin_thread=true) SimpleActionClient (const std::string &name, bool spin_thread=true)
我们总是疑惑系统默认指定的NodeHandle和我们自己指定的NodeHandle有啥区别呢?这需要我们从NodeHandle的作用来进行说明:NodeHandle的作用是在master主节点注册本node的信息以及建立通信所需的topic,除此之外,NodeHandle还可以指定namespace:
ros::NodeHandle nh("/my_global_namespace"); // 指定一个全局域名
actionlib::SimpleActionClient obj(nh,"chatter",true);
2. “等待此客户端连接到动作服务器”的函数:
bool waitForServer (const ros::Duration &timeout=ros::Duration(0, 0))
Duration(0,0)的含义是:client无限期等待server上线。其中参数表示的是等待时间:
obj.waitForServer(ros::Duration(3));//等待3s
3. “检查是否成功连上动作服务器”的函数:
bool isServerConnected ();
4. “发送一个目标值到动作服务器,然后等待直到目标完成或者超时”的函数:
SimpleClientGoalState sendGoalAndWait (const Goal &goal, const ros::Duration &execute_timeout=ros::Duration(0, 0), const ros::Duration &preempt_timeout=ros::Duration(0, 0))
例如:如若3s内完成不了goal,client就会发送一个新的goal去取代旧的goal
obj.sendGoalAndWait(goal,ros::Duration(0, 0),ros::Duration(3));
5. “等待直至目标完成或者在指定的时间内等待result”的函数:
bool waitForResult (const ros::Duration &timeout=ros::Duration(0, 0))
该阻塞函数的意义在于:在指定时间内等待server回传给client端result或者一直等待直至server回传给client端result。
6. “获得通信状态”的函数:
SimpleClientGoalState getState ();
示例:
if (obj.getState() == actionlib::SimpleClientGoalState::SUCCEEDED)
ROS_INFO("You have reached the goal!");
7. 其他成员函数:
获取当前目标的结果: ResultConstPtr getResult () 取消正在动作服务器上运行的所有目标: void cancelAllGoals () 取消我们当前正在追踪的的目标: void cancelGoal () 取消在指定时间之前和之前标记的所有目标: void cancelGoalsAtAndBeforeTime (const ros::Time &time)
客户端程序——actionClient.cpp
#include "actionlib/client/simple_action_client.h"
#include "actionProject/washAction.h"
#include "ros/ros.h"
void SimpleDoneCallbackFunc(const actionlib::SimpleClientGoalState& state,const actionProject::washResultConstPtr& result)
{
ROS_INFO("finish!nt");
ROS_INFO("Now State:%dnt",state.state_);
ROS_INFO("total_washed_dished:%dnt",result->total_washed_dished);
ros::shutdown(); // shut down the operation of node explicitly
}
void SimpleActiveCallbackFunc()
{
ROS_INFO("the node is active!nt");
}
void SimpleFeedbackCallbackFunc(const actionProject::washFeedbackConstPtr& feedback)
{
ROS_INFO("now_number_washed_dished:%dnt",feedback->now_number_washed_dished);
}
int main(int argc,char* argv[])
{
// localize
setlocale(LC_ALL,"");
// initial the node
ros::init(argc,argv,"actionClient");
// define ActionClient-type object
ros::NodeHandle nh;
actionlib::SimpleActionClient obj(nh,"chatter",true);
// wait until the server appears
ROS_INFO("waiting for action server to start.");
obj.waitForServer();
ROS_INFO("Action server started, sending goal.");
// pack data
actionProject::washGoal goal;
goal.targeted_washed_dishes = 10;
// submit data to ActionServer
obj.sendGoal(goal,&SimpleDoneCallbackFunc,&SimpleActiveCallbackFunc,&SimpleFeedbackCallbackFunc);
// loop continueously to receive information
ros::spin(); // when finishing goal, shutdown() will end the node
return 0;
}
服务端.cpp源文件的构建
基本流程
① 初始化节点
ros::init(argc,argv,"actionServer");
一个节点就是一个.cpp源文件,习惯上来说节点名称和源文件名称一致,有些人疑惑“节点的名称和.cpp源文件的名称有啥关系呢?”其实,二者并没有任何关系。我们调用rqt_graph显示出的节点之间的关系图,上面的节点的名称就是我们节点初始化时的名称。.cpp源文件的名称我们在配置CMakelist.txt文件中用到过,编译器链接器根据.cpp源文件的名称将其编译链接为CMake文件。
② 在主节点注册server信息以及声明回调函数
// create the ActionServer-type object ros::NodeHandle nh; actionlib::SimpleActionServer obj(nh,"chatter",boost::bind(&ExecuteCallback,_1,&obj),false);
其实,actionlib::SimpleActionServer类对象的初始化方式有多种:
SimpleActionServer(ros::NodeHandle n, std::string name, ExecuteCallback execute_callback, bool auto_start); SimpleActionServer(std::string name, ExecuteCallback execute_callback, bool auto_start); // 使用默认节点句柄 SimpleActionServer(ros::NodeHandle n, std::string name, bool auto_start); // 省去了回调函数 SimpleActionServer(std::string name, bool auto_start); // 使用默认节点句柄且省去了回调函数
其实,回调函数的作用就是处理从client传来的信息,我们前面提到过clientàserver传递的信息一共有两个:goal和cancel。其中,goal信息标志着client向server发送的请求,因此当server收到从client发来的goal信息时,就会通过ros::spin()调用回调函数。回调函数如果没有的话,就说明server不会对client传来的请求给予响应。
最后一个函数为bool auto_start:true表征着“server节点自启动”,false则代表“我们必须手动启动server节点”。其实,自动启动中“自动”的含义就是:我们在设置为true之后,就无需在使用服务端的成员函数obj.start()手动自启动了。
③ 启动server
obj.start();
我们应该注意到:client和server启动方式不同(为什么在客户端没有start函数呢?)。其原因就在于通信双方的角色不同,client担当请求的发起者,而server担当请求的承受者。因此,当client发送goal信息时就表明client已经发起了此次通信的建立,而server只有自己调用start成员函数启动才可以接收到来自client的请求从而被动的与其建立联系。
除此之外,如果server端的actionlib::SimpleActionServer::SimpleActionServer实例化对象时最后一个参数为true则不用再使用actionlib::SimpleActionServer::SimpleActionServer实例化对象调用start成员函数启动server了,因为auto_start=true代表着server节点自启动。
自启动和手动启动的区别:
其实都是启动server节点的数据接收功能,使其可以接收到来自client的信息并进行处理。但是自启动的含义是:当server向master上报自己信息之后紧接着就会启动请求接收功能,但是如果我们不想立刻启动server的数据接收功能,即有条件的启动server数据接收功能呢?这就得需要我们调用start函数,在有需要的时候在开启server的数据接收功能。与start函数对应的是shutdown函数,shutdown函数的作用:关闭server接收来自client请求的功能。
④ 调用阻塞函数以便实时接收来自client的请求从而给出响应
ros::spin();
调用回调函数的两个条件:正在执行ros::spin()阻塞函数+server接收到来自client的goal信息,满足这两个条件,才会调用server端的回调函数。
⑤ 回调函数的编写
首先,我们要明确回调函数的作用:通过收到来自client的goal请求信号,产生响应向client发送feedback、state、result。回调函数的功能大致如下:
函数的编写逻辑如下:
1. 设置一套响应的方案;
2. 按照一定的频率上报feedback;
3. 完成后,将state设置为SUCCEEDED
相应的代码如下:
void ExecuteCallback(const actionProject::washGoalConstPtr& goalptr,actionlib::SimpleActionServer* ptr)
{
ros::Rate r(1);
actionProject::washFeedback feedback;
ROS_INFO("final aim:%dnt",goalptr->targeted_washed_dishes);
// submit feedback
for(int i=0;i<=goalptr->targeted_washed_dishes;i++)
{
feedback.now_number_washed_dished = i;
ptr->publishFeedback(feedback);
r.sleep();
}
ROS_INFO("finished number:%dnt",feedback.now_number_washed_dished);
ptr->setSucceeded();
}
这里的响应逻辑很简单就是不断地加一而已,但是一定要注意从client传来的goal信息必须作为我们评价工作是否完成的依据,只有达到client请求的目标我们才可以设置state为SUCCEEDED。如下所示:
// submit feedback
for(int i=0;i<=goalptr->targeted_washed_dishes;i++)
{
feedback.now_number_washed_dished = i;
ptr->publishFeedback(feedback);
r.sleep();
}
只有当feedback.now_number_washed_dished= goalptr->targeted_washed_dishes时,循环才会结束(server响应才会结束),此时才会调用ptr->setSucceeded()将state置位SUCCEEDED。
注意函数的两个入口参数:
| const actionProject::washGoalConstPtr& goalptr | 从client传来的goal信息 |
| actionlib::SimpleActionServer* ptr | 用于向client发布信息 |
比较容易忽略的是函数入口参数类型的确定,我们可以参考函数类型的原型来确定参数类型:
typedef boost::functionExecuteCallback;
我当时就是因为漏掉了const导致编译失败但却找不到原因所在。
actionlib::SimpleActionServer的成员函数
① 轮询判断函数
轮询判断server是否正在执行当前client的请求 bool isActive() 轮询判断server是否接收到来自client的新请求 bool isNewGoalAvailable() 轮询判断server是否接受到抢占的请求 bool isPreemptRequested()
② server向client发送feedback数据的函数
void publishFeedback (const FeedbackConstPtr &feedback) void publishFeedback (const Feedback &feedback)
③ 注册回调函数的函数
注册“接收到来自client请求信息时的回调函数” void registerGoalCallback (boost::function< void()> cb) 注册“接收到抢占信号的回调函数” void registerPreemptCallback (boost::function< void()> cb)
我们前面提到过actionlib::SimpleActionServer对象的初始化方式中有“可以无需在创建对象时就指定回调函数”的构造函数。我们在声明时不指定回调函数,可以在创建完对象之后再指定回调函数。
④ state状态机设置函数
// 将state置为ABORTED——由于某些故障,操作服务器在执行过程中中止了目标
void setAborted (const Result &result=Result(), const std::string &text=std::string(""))
// 将state置为PREEMPTED——目标在开始执行后收到取消请求,并已完成执行
void setPreempted (const Result &result=Result(), const std::string &text=std::string(""))
// 将state置为SUCCEEDED——动作服务器成功地实现了目标
void setSucceeded (const Result &result=Result(), const std::string &text=std::string(""))
使用示例:
actionlib::SimpleActionServer::Result result; ptr->setSucceeded(result,"SUCCEEDEDnt");
⑤ 中止/启动server函数
// 中止服务端的运行 void shutdown () // 启动服务端的运行 void start ()
⑥ 用于“接收来自client的新的goal请求信息“的函数
templateboost::shared_ptr SimpleActionServer ::acceptNewGoal()
顾名思义,这个函数是用来接收从client发来的新的goal请求信息的。在使用这个函数时一定要特别注意:isNewGoalAvailable、acceptNewGoal、isPreemptRequested这三个函数的使用次序。三者的作用分别为:
| isNewGoalAvailable | 判断server是否已经接收到来自client的新请求 |
| acceptNewGoal | 接收一个来自client的新请求 |
| isPreemptRequested | 判断旧的goal被新的goal抢占了吗 |
官方文档特别标注三者的使用次序:
当从client发来的新的goal请求信息已经覆盖了旧的goal请求信息,server端才会调用抢占回调函数。“从client发来的新的goal请求信息已经覆盖了旧的goal请求信息”的标志就是obj.isNewGoalAvailable()=true(obj是actionlib::SimpleActionServer的模板实例化对象),只有在判断obj.isNewGoalAvailable()之后,我们才可以用obj.isPreemptRequested()来判断旧的goal是否被抢占,如果obj.isNewGoalAvailable()=true并且我们使用函数obj.registerPreemptCallback (boost::function< void()> cb)注册了抢占回调函数,那么在新goal覆盖旧goal之后,即obj.isNewGoalAvailable()=true之后,server端会立刻调用抢占回调函数。
服务端程序——actionServer.cpp
#include "actionlib/server/simple_action_server.h"
#include "actionProject/washAction.h"
#include "ros/ros.h"
void ExecuteCallback(const actionProject::washGoalConstPtr& goalptr,actionlib::SimpleActionServer* ptr)
{
ros::Rate r(1);
actionProject::washFeedback feedback;
ROS_INFO("final aim:%dnt",goalptr->targeted_washed_dishes);
// submit feedback
for(int i=0;i<=goalptr->targeted_washed_dishes;i++)
{
feedback.now_number_washed_dished = i;
ptr->publishFeedback(feedback);
r.sleep();
}
ROS_INFO("finished number:%dnt",feedback.now_number_washed_dished);
ptr->setSucceeded();
}
int main(int argc, char* argv[])
{
// localize
setlocale(LC_ALL,"");
// initial the node
ros::init(argc,argv,"actionServer");
// create the ActionServer-type object
ros::NodeHandle nh;
actionlib::SimpleActionServer obj(nh,"chatter",boost::bind(&ExecuteCallback,_1,&obj),false);
obj.start();
ros::spin();
return 0;
}
#include "actionlib/server/simple_action_server.h"
#include "actionProject/washAction.h"
#include "ros/ros.h"
void ExecuteCallback(const actionProject::washGoalConstPtr& goalptr,actionlib::SimpleActionServer* ptr)
{
ros::Rate r(1);
actionProject::washFeedback feedback;
ROS_INFO("final aim:%dnt",goalptr->targeted_washed_dishes);
// submit feedback
for(int i=0;i<=goalptr->targeted_washed_dishes;i++)
{
feedback.now_number_washed_dished = i;
ptr->publishFeedback(feedback);
r.sleep();
}
ROS_INFO("finished number:%dnt",feedback.now_number_washed_dished);
ptr->setSucceeded();
}
int main(int argc, char* argv[])
{
// localize
setlocale(LC_ALL,"");
// initial the node
ros::init(argc,argv,"actionServer");
// create the ActionServer-type object
ros::NodeHandle nh;
actionlib::SimpleActionServer obj(nh,"chatter",boost::bind(&ExecuteCallback,_1,&obj),false);
obj.start();
ros::spin();
return 0;
}
附加知识
我们在编写action通信双方的源文件时一定要注意:
① 在编写client源文件时
// define ActionClient-type object ros::NodeHandle nh; actionlib::SimpleActionClient obj(nh,"chatter",true);
spin_thread参数是true
② 在编写server源文件时
// create the ActionServer-type object ros::NodeHandle nh; actionlib::SimpleActionServer obj(nh,"chatter",boost::bind(&ExecuteCallback,_1,&obj),false);
auto_start参数是false
一旦两者均为true,则运行之后在server端会出现如下警示:
You've passed in true for auto_start for the C++ action server at [/topic_name]. You should always pass in false to avoid race conditions.
原因如下:
在官方的文档说明中,明确指出
server端的auto_start=false,如果设置为true会引发race conditions,具体请参考:Race Condition(竞争条件)_涂涂的博客-CSDN博客_竞争条件https://blog.csdn.net/u012562273/article/details/56486776#:~:text=Race%20Condition%20Race%20Condition%20%EF%BC%88,%E7%AB%9E%E4%BA%89%E6%9D%A1%E4%BB%B6%20%EF%BC%89%E6%98%AF%E4%B8%80%E7%A7%8D%E6%83%85%E5%BD%A2%EF%BC%8C%E5%9C%A8%E8%AF%A5%E6%83%85%E5%BD%A2%E4%B8%8B%E7%B3%BB%E7%BB%9F%E6%88%96%E8%80%85%E7%A8%8B%E5%BA%8F%E7%9A%84%E8%BE%93%E5%87%BA%E5%8F%97%E5%85%B6%E4%BB%96%E4%B8%8D%E5%8F%AF%E6%8E%A7%E4%BA%8B%E4%BB%B6%E7%9A%84%E9%A1%BA%E5%BA%8F%E6%88%96%E4%BA%8B%E4%BB%B6%E7%9A%84%E5%BD%B1%E5%93%8D%E3%80%82%20%E8%BD%AF%E4%BB%B6%E4%B8%AD%E7%9A%84%20Race%20Condition%20%E9%80%9A%E5%B8%B8%E5%87%BA%E7%8E%B0%E5%9C%A8%E4%B8%A4%E4%B8%AA%E5%B9%B6%E5%8F%91%E7%BA%BF%E7%A8%8B%E8%AE%BF%E9%97%AE%E5%90%8C%E4%B8%80%E4%B8%AA%E5%85%B1%E4%BA%AB%E8%B5%84%E6%BA%90%E3%80%82
由于两个或者多个进程竞争使用不能被同时访问的资源,使得这些进程有可能因为时间上推进的先后原因而出现问题,这叫做竞争条件(Race Condition)。。
相关知识解析
捆绑函数boost::bind的使用
int f(int a, int b)
{
return a + b;
}
int g(int a, int b, int c)
{
return a + b + c;
}
int f(int a, int b)
{
return a + b;
}
int g(int a, int b, int c)
{
return a + b + c;
}
普通用法:函数传参
bind(f, 1, 2)等价于f(1, 2); bind(g, 1, 2, 3)等价于g(1, 2, 3);
灵活用法:有选择性地绑定参数
bind(f, _1, 5)(x)等价于f(x, 5),其中_1是一个占位符,表示用第一个参数来替换;
bind(f, _2, _1)(x, y)等价于f(y, x);
bind(g, _1, 9, _1)(x)等价于g(x, 9, x);
bind(g, _3, _3, _3)(x, y, z)等价于g(z, z, z);
最终运行结果
① server端:
② client端:



