在 ROS(Robot Operating System)程序中,如果 HMI 服务器端的所有 topic 都在同一个线程内接收,并且多个 topic 的回调函数耗时超过 10ms,可能会导致消息处理延迟,影响实时性。以下是一些优化的方法和建议:

问题分析

  1. 单线程瓶颈:ROS 默认使用单线程处理所有 topic 的回调函数(ros::spin() 或 ros::spinOnce())。如果某个回调函数耗时较长,会阻塞其他回调函数的执行,导致延迟。
  2. 回调函数耗时:某些 topic 的回调函数可能涉及复杂计算、I/O 操作或阻塞调用,导致耗时超过 10ms。
  3. 实时性要求: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;

    // 订阅多个 topic

    ros::Subscriber sub1 = nh.subscribe("topic1", 10, callback1);

    ros::Subscriber sub2 = nh.subscribe("topic2", 10, callback2);

    // 使用多线程 Spinner

    ros::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 的消息时触发回调,减少回调频率。

推荐方案

  1. 优先尝试:使用 ros::MultiThreadedSpinner(方案 1),简单高效,适合大多数场景。
  2. 耗时严重时:结合消息队列和工作线程(方案 2),将耗时操作移出回调函数。
  3. 复杂系统:考虑分离 HMI 和 ROS 逻辑(方案 5),提高模块化和可维护性。

注意事项

  • 线程安全:多线程方案需确保数据访问安全,使用锁或原子操作。
  • 性能监控:定期监控回调函数耗时和系统负载,优化瓶颈。
  • 测试:在优化后进行压力测试,确保系统稳定性和实时性。
  • ROS 版本:确认使用的是 ROS 1 或 ROS 2,代码细节可能略有不同(上述示例基于 ROS 1)。
Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐