所有 topic 都在同一个线程内接收
在 ROS(Robot Operating System)程序中,如果 HMI 服务器端的所有 topic 都在同一个线程内接收,并且多个 topic 的回调函数耗时超过 10ms,可能会导致消息处理延迟,影响实时性。以下是一些优化的方法和建议:
问题分析
- 单线程瓶颈:ROS 默认使用单线程处理所有 topic 的回调函数(ros::spin() 或 ros::spinOnce())。如果某个回调函数耗时较长,会阻塞其他回调函数的执行,导致延迟。
- 回调函数耗时:某些 topic 的回调函数可能涉及复杂计算、I/O 操作或阻塞调用,导致耗时超过 10ms。
- 实时性要求:HMI 通常需要快速响应用户交互或实时数据,10ms 以上的延迟可能影响用户体验。
优化方案
以下是针对该问题的优化建议,按照优先级和实现复杂度排序:
1. 使用多线程 Spinner
ROS 提供了 ros::MultiThreadedSpinner 或 ros::AsyncSpinner,可以利用多线程来并行处理回调函数,减少单线程阻塞。
实现方法:
- 将 ros::spin() 替换为 ros::MultiThreadedSpinner 或 ros::AsyncSpinner。
- 示例代码:
cpp
#include <ros/ros.h>#include <std_msgs/String.h>void callback1(const std_msgs::String::ConstPtr& msg) {// 耗时操作ROS_INFO("Received on topic1: %s", msg->data.c_str());ros::Duration(0.02).sleep(); // 模拟耗时 20ms}void callback2(const std_msgs::String::ConstPtr& msg) {ROS_INFO("Received on topic2: %s", msg->data.c_str());}int main(int argc, char** argv) {ros::init(argc, argv, "hmi_node");ros::NodeHandle nh;// 订阅多个 topicros::Subscriber sub1 = nh.subscribe("topic1", 10, callback1);ros::Subscriber sub2 = nh.subscribe("topic2", 10, callback2);// 使用多线程 Spinnerros::MultiThreadedSpinner spinner(4); // 4 个线程,可根据需要调整spinner.spin(); // 启动多线程处理return 0;} - 说明:
- ros::MultiThreadedSpinner 允许多个线程并行处理回调函数,适合有多个 topic 的场景。
- ros::AsyncSpinner 是异步的,适合不想阻塞主线程的场景。
- 线程数可以根据 CPU 核心数和 topic 数量调整(通常 2-4 线程足够)。
注意事项:
- 确保回调函数是线程安全的,避免共享资源竞争(如全局变量)。
- 如果回调函数需要访问共享资源,使用 std::mutex 或 ROS 的 boost::mutex 进行保护。
2. 优化回调函数的耗时操作
如果回调函数本身耗时过长,可以将耗时操作移到单独的线程或队列中处理。
实现方法:
- 使用消息队列存储接收到的消息,回调函数只负责快速存储数据。
- 在单独的工作线程中处理耗时操作。
- 示例代码:
cpp
#include <ros/ros.h>#include <std_msgs/String.h>#include <queue>#include <thread>#include <mutex>std::queue<std_msgs::String::ConstPtr> message_queue;std::mutex queue_mutex;void callback(const std_msgs::String::ConstPtr& msg) {// 快速存储消息到队列std::lock_guard<std::mutex> lock(queue_mutex);message_queue.push(msg);}void worker_thread() {while (ros::ok()) {std_msgs::String::ConstPtr msg;{std::lock_guard<std::mutex> lock(queue_mutex);if (!message_queue.empty()) {msg = message_queue.front();message_queue.pop();}}if (msg) {// 耗时处理ROS_INFO("Processing: %s", msg->data.c_str());ros::Duration(0.02).sleep(); // 模拟耗时 20ms}ros::Duration(0.001).sleep(); // 避免 CPU 占用过高}}int main(int argc, char** argv) {ros::init(argc, argv, "hmi_node");ros::NodeHandle nh;ros::Subscriber sub = nh.subscribe("topic", 10, callback);// 启动工作线程std::thread worker(worker_thread);ros::spin(); // 主线程处理 ROS 回调worker.join(); // 等待工作线程结束return 0;} - 说明:
- 回调函数只负责将消息存入队列,耗时极短。
- 工作线程独立处理耗时操作,避免阻塞 ROS 回调。
注意事项:
- 使用线程安全的队列(如 std::queue 配合 std::mutex)。
- 确保工作线程的处理速度能跟上消息到达速度,避免队列积压。
3. 调整 topic 的队列大小
如果消息到达频率高,而回调函数处理速度跟不上,可能会导致消息丢失。可以通过增加 topic 的队列大小来缓解。
实现方法:
- 在订阅 topic 时设置更大的队列大小(默认是 10)。
- 示例:
cpp
ros::Subscriber sub = nh.subscribe("topic", 100, callback); // 队列大小设为 100
注意事项:
- 增大队列会增加内存占用,需权衡。
- 这只是缓解措施,不能根本解决耗时问题。
4. 使用 ros::Timer 定期处理
如果某些 topic 的消息不需要立即处理,可以使用 ros::Timer 定期批量处理,降低回调频率。
实现方法:
- 示例代码:
cpp
#include <ros/ros.h>#include <std_msgs/String.h>#include <vector>std::vector<std_msgs::String::ConstPtr> messages;void callback(const std_msgs::String::ConstPtr& msg) {messages.push_back(msg); // 快速存储消息}void timerCallback(const ros::TimerEvent&) {// 定期处理消息for (const auto& msg : messages) {ROS_INFO("Processing: %s", msg->data.c_str());ros::Duration(0.02).sleep(); // 模拟耗时}messages.clear();}int main(int argc, char** argv) {ros::init(argc, argv, "hmi_node");ros::NodeHandle nh;ros::Subscriber sub = nh.subscribe("topic", 10, callback);ros::Timer timer = nh.createTimer(ros::Duration(0.1), timerCallback); // 每 100ms 处理一次ros::spin();return 0;} - 说明:
- 回调函数只存储消息,定时器定期处理。
- 适合对实时性要求不高的场景。
注意事项:
- 定时器的频率需要根据消息到达速率和处理时间调整。
- 可能增加延迟,需权衡实时性需求。
5. 分离 HMI 和 ROS 逻辑
如果 HMI 的逻辑复杂,考虑将 HMI 和 ROS 通信分开,使用独立进程或节点通信。
实现方法:
- 将 ROS 节点和 HMI 逻辑分为两个进程,通过 ROS topic 或服务通信。
- HMI 进程使用轻量级框架(如 Qt 或 WebSocket)处理用户界面,ROS 节点专注于消息处理。
- 示例:
- ROS 节点订阅 topic,处理后通过 topic 或服务将结果发送给 HMI 节点。
- HMI 节点只负责显示和用户交互。
优点:
- HMI 进程不受 ROS 回调阻塞影响。
- 提高模块化,便于维护。
缺点:
- 增加系统复杂性,需实现进程间通信。
6. 分析和优化回调函数
检查回调函数中的耗时操作,尝试优化代码:
- 减少复杂计算:将复杂算法分解或使用更高效的实现。
- 避免阻塞调用:如网络请求、文件 I/O 等,移到单独线程。
- 使用缓存:对于重复计算的结果,使用缓存避免重复处理。
- 性能分析:使用 ros::WallTimer 或 std::chrono 测量回调函数的耗时,定位瓶颈。
示例:
cpp
void callback(const std_msgs::String::ConstPtr& msg) {
ros::WallTime start = ros::WallTime::now();
// 耗时操作
ros::Duration(0.02).sleep();
ros::WallTime end = ros::WallTime::now();
ROS_INFO("Callback took %f ms", (end - start).toSec() * 1000);
}
7. 调整 ROS 消息频率
如果 topic 的消息发布频率过高,可以与发布端协商降低频率,或者使用 message_filters 进行消息过滤。
示例(使用 message_filters::TimeSynchronizer 同步多个 topic):
cpp
#include <message_filters/subscriber.h>
#include <message_filters/time_synchronizer.h>
void callback(const std_msgs::String::ConstPtr& msg1, const std_msgs::String::ConstPtr& msg2) {
ROS_INFO("Synced messages: %s, %s", msg1->data.c_str(), msg2->data.c_str());
}
int main(int argc, char** argv) {
ros::init(argc, argv, "hmi_node");
ros::NodeHandle nh;
message_filters::Subscriber<std_msgs::String> sub1(nh, "topic1", 10);
message_filters::Subscriber<std_msgs::String> sub2(nh, "topic2", 10);
message_filters::TimeSynchronizer<std_msgs::String, std_msgs::String> sync(sub1, sub2, 10);
sync.registerCallback(boost::bind(&callback, _1, _2));
ros::spin();
return 0;
}
- 说明:只在收到所有 topic 的消息时触发回调,减少回调频率。
推荐方案
- 优先尝试:使用 ros::MultiThreadedSpinner(方案 1),简单高效,适合大多数场景。
- 耗时严重时:结合消息队列和工作线程(方案 2),将耗时操作移出回调函数。
- 复杂系统:考虑分离 HMI 和 ROS 逻辑(方案 5),提高模块化和可维护性。
注意事项
- 线程安全:多线程方案需确保数据访问安全,使用锁或原子操作。
- 性能监控:定期监控回调函数耗时和系统负载,优化瓶颈。
- 测试:在优化后进行压力测试,确保系统稳定性和实时性。
- ROS 版本:确认使用的是 ROS 1 或 ROS 2,代码细节可能略有不同(上述示例基于 ROS 1)。
更多推荐



所有评论(0)