[ROS2 Multi Thread] Multi Thread의 필요성

2025. 5. 31. 01:07·ROS2

1. 개요

ROS2에서는 하나의 노드에서 여러 개의 토픽을 구독하거나, 다양한 센서 데이터를 동시에 처리해야 하는 경우가 많다. 이때, 시스템이 어떻게 콜백을 처리하는지에 따라 전체 성능과 반응 속도에 큰 영향을 미친다.

 

ROS2의 기본 실행 모델은 SingleThreadedExecutor이기 때문에, 아무리 많은 토픽을 구독하더라도 콜백은 하나씩 순차적으로 처리된다. 이로 인해 여러 센서 데이터가 동시에 들어와도 처리 지연(latency)이 발생할 수 있다. 이러한 문제를 해결하기 위한 방법이 바로 멀티 스레드(Multi-Threading)이다.

 

2. 상황 예제 코드

다음은 하나의 노드에서 IMU, Odometry 또는 PoseStamped 데이터를 구독하고, 일정 주기로 통합 처리를 수행하는 예제이다.

여러개의 Topic을 callback 함수를 통하여 subscribe 하는 극단적인 상황 예이다.

 

#include <iostream>
#include <memory>
#include <chrono>
#include <functional>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
#include "geometry_msgs/msg/pose_stamped.hpp"
#include "mk3_msgs/msg/navigation_type.hpp"
#include "sensor_msgs/msg/imu.hpp"
#include "nav_msgs/msg/odometry.hpp"

using std::placeholders::_1;
using namespace std::chrono_literals;

class odom_navigation : public rclcpp::Node
{
public:
  odom_navigation()
  : Node("odom_navigation")
  {
    navigation_publisher_ = this->create_publisher<mk3_msgs::msg::NavigationType>("navigation", 10); 
    timer_ = this->create_wall_timer(100ms, std::bind(&odom_navigation::process, this));  // 주기적으로 처리

    imu_subscription_ = this->create_subscription<sensor_msgs::msg::Imu>(
      "/ouster/imu", 10, std::bind(&odom_navigation::imu_callback, this, _1));

    use_odometry = false;
    if (use_odometry)
    {
      odom_subscription_ = this->create_subscription<nav_msgs::msg::Odometry>(
        "odometry", 10, std::bind(&odom_navigation::odometry_callback, this, _1));
    }
    else
    {
      pose_subscription_ = this->create_subscription<geometry_msgs::msg::PoseStamped>(
        "current_pose", 10, std::bind(&odom_navigation::pose_callback, this, _1));
    }
  }

private:
  void process()
  {
    RCLCPP_INFO(this->get_logger(), "Processing data...");

    // 수신한 센서 데이터를 활용하여 통합 처리
    if (last_imu_ && (use_odometry ? last_odom_ : last_pose_))
    {
      mk3_msgs::msg::NavigationType msg;
      msg.header.stamp = this->now();
      msg.yaw_rate = last_imu_->angular_velocity.z;
      if (use_odometry)
        msg.vel_x = last_odom_->twist.twist.linear.x;
      else
        msg.pos_x = last_pose_->pose.position.x;

      navigation_publisher_->publish(msg);
    }
  }

  void odometry_callback(const nav_msgs::msg::Odometry::SharedPtr msg)
  {
    RCLCPP_INFO(this->get_logger(), "Received Odom");
    last_odom_ = msg;
  }

  void pose_callback(const geometry_msgs::msg::PoseStamped::SharedPtr msg)
  {
    RCLCPP_INFO(this->get_logger(), "Received Pose");
    last_pose_ = msg;
  }

  void imu_callback(const sensor_msgs::msg::Imu::SharedPtr msg)
  {
    RCLCPP_INFO(this->get_logger(), "Received IMU");
    last_imu_ = msg;
  }

  bool use_odometry;
  rclcpp::TimerBase::SharedPtr timer_;
  rclcpp::Subscription<sensor_msgs::msg::Imu>::SharedPtr imu_subscription_;
  rclcpp::Subscription<nav_msgs::msg::Odometry>::SharedPtr odom_subscription_;
  rclcpp::Subscription<geometry_msgs::msg::PoseStamped>::SharedPtr pose_subscription_;
  rclcpp::Publisher<mk3_msgs::msg::NavigationType>::SharedPtr navigation_publisher_;

  // 수신한 데이터를 저장할 멤버 변수
  sensor_msgs::msg::Imu::SharedPtr last_imu_;
  nav_msgs::msg::Odometry::SharedPtr last_odom_;
  geometry_msgs::msg::PoseStamped::SharedPtr last_pose_;
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<odom_navigation>());
  rclcpp::shutdown();
  return 0;
}

 

3. 싱글 스레드 모델의 특징

기본적으로 아래와 같은 형태로 작성된 노드는 싱글 스레드 기반으로 동작한다.

rclcpp::spin(std::make_shared<odom_navigation>());

 

이때 사용되는 SingleThreadedExecutor의 동작 방식은 다음과 같다:

  • 콜백을 하나씩 순차적으로 실행한다.
  • 하나의 콜백 함수가 실행 중이면, 다른 콜백은 큐에 대기한다.
  • CPU 코어가 여럿 있더라도, 실제 처리는 한 스레드만 수행한다.
  • 구조가 단순하고 디버깅이 쉬우며, 동기화 문제가 발생하지 않는다.
  • 하지만, 센서 데이터가 많은 경우 병목 현상이 발생할 수 있다.

예를 들어, IMU와 Odometry, PoseStamped 데이터를 동시에 구독할 경우, IMU 콜백이 실행되는 동안 다른 콜백은 큐에 쌓여 순서대로 처리된다. 콜백 처리 시간이 길어지면 다른 센서 데이터도 늦게 처리되므로 실시간성이 떨어진다.

 

4. 싱글 스레드 사용 시 발생할 수 있는 문제

이 예제에서 SingleThreadedExecutor를 사용하면 다음과 같은 문제가 발생할 수 있다:

  • 콜백 함수들이 순차적으로 실행됨: imu_callback, pose_callback, process()는 동시에 실행되지 않는다.
  • imu_callback()이 오래 걸리거나 블로킹 연산을 포함하면, process()나 pose_callback()이 지연된다.
  • 센서 데이터가 빠르게 들어오는 경우, 처리 지연(latency) 으로 인해 데이터 유실이나 동기화 오류가 생길 수 있다.
  • process() 주기보다 빠른 센서 입력이 쌓이면서 큐 오버플로우 또는 지연 전파가 발생할 수 있다.

5. 멀티 스레드 모델의 필요성

이를 해결하기 위해 ROS2에서는 MultiThreadedExecutor를 제공한다.

 

멀티 스레드 실행 방식:

rclcpp::executors::MultiThreadedExecutor executor;
executor.add_node(node);
executor.spin();

 

장점:

  • 여러 개의 콜백을 동시에 병렬 실행할 수 있다.
  • 각 콜백이 별도의 스레드에서 실행되므로, 콜백 처리 지연이 줄어든다.
  • 멀티 코어 CPU를 적극적으로 활용 가능하다.

단점:

  • 동시에 실행되는 콜백 간에 데이터 충돌 가능성이 존재한다.
  • 공유 자원 보호를 위한 mutex 또는 atomic 변수 사용이 필요하다.
  • 디버깅이 복잡해지고, 콜백 실행 순서가 예측 불가능할 수 있다.

6. 예제 상황에서의 멀티 스레드 사용 시 장점

예제 상황에서 MultiThreadedExecutor를 사용하면 다음과 같은 이점이 있다:

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  auto node = std::make_shared<odom_navigation>();
  rclcpp::executors::MultiThreadedExecutor executor;
  executor.add_node(node);
  executor.spin();
  rclcpp::shutdown();
  return 0;
}

 

장점:

  • 콜백 함수가 병렬로 실행되므로, IMU 수신 도중에도 process()나 pose_callback()이 실행 가능하다.
  • 각 콜백이 별도 스레드에서 실행되어, 센서 간 독립적인 처리가 가능하다.
  • 멀티 코어 CPU를 효과적으로 활용하여 처리 속도를 높인다.
  • 실시간성 개선 및 지연 감소.

7. 멀티 스레드 사용 시 주의할 점

멀티 스레드는 성능을 높이지만, 다음과 같은 문제를 유발할 수 있다:

  • 콜백 간에 공유 자원(last_imu_, last_pose_ 등) 을 동시에 접근할 수 있으므로 race condition 발생 가능성 있음.
  • 따라서 공유 자원 접근 시 std::mutex 또는 rclcpp::GuardCondition 등을 이용한 동기화가 필수.
  • 콜백 순서가 보장되지 않기 때문에, 시간 스탬프를 기반으로 적절한 데이터 동기화 전략이 필요함.

8. SingleThreadedExecutor vs MultiThreadedExecutor 비교

콜백 실행 하나씩 순차 처리 병렬 실행 가능
콜백 처리 속도 느림 (병목 가능성 있음) 빠름 (실시간성 향상)
동기화 문제 없음 있음 (mutex 등 필요)
구현 난이도 쉬움 어려움
적합한 상황 단일 센서, 낮은 빈도 다중 센서, 고빈도, 실시간성 요구

 

9. 결론

SingleThreadedExecutor는 구조가 간단하고 안전하지만, 콜백 처리가 병목을 일으킬 수 있다. 실시간성이 중요한 시스템이나 다중 센서 환경에서는 MultiThreadedExecutor를 사용하여 병렬 처리를 적용하는 것이 필요하다. 단, 이 경우 공유 자원에 대한 철저한 동기화 설계가 요구된다.

멀티 스레드를 사용하는 것이 항상 정답은 아니며, 시스템 요구사항과 데이터 흐름, 리소스 특성에 따라 적절한 실행 모델을 선택하는 것이 중요하다.

멀티 스레드는 강력한 도구이지만, 동기화와 구조적 설계를 신중히 고려해야만 안정적이고 빠른 시스템을 만들 수 있다.

 

 

※ 본 글은 필자가 공부를 하며 정리한 내용입니다. 차후 더 자세한 예제와 활용 방식에 대하여 포스팅 예정입니다. 감사합니다. 

 

'ROS2' 카테고리의 다른 글

[ROS2 GPS, IMU 항법 코드] ROS2 기반 GPS + IMU 항법 시스템 구현  (2) 2025.06.04
'ROS2' 카테고리의 다른 글
  • [ROS2 GPS, IMU 항법 코드] ROS2 기반 GPS + IMU 항법 시스템 구현
lidarmansiwon
lidarmansiwon
lidarmansiwon 님의 블로그 입니다.
  • lidarmansiwon
    라이다맨 시원의 연구개발 라이프
    lidarmansiwon
  • 전체
    오늘
    어제
    • 분류 전체보기 (12)
      • 이론 정리 (2)
        • Thor I. Fossen 리뷰 (1)
      • Ubuntu 및 Linux (0)
        • Trouble shooting (0)
      • 개발 언어 (5)
        • C++ (5)
        • Python (0)
      • 논문 리뷰 (3)
      • ROS2 (2)
  • 블로그 메뉴

    • Github
    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    marinecraft
    singlethreadedexecutor
    슬라이딩 모드 컨트롤
    usv formation
    자율선박
    cpp
    C++
    ROS2
    c++ 기초부터 심화까지
    usv formation path planning based on behavior trees and fast marching method
    fossen
    navigationcontrol
    motioncontrol
    do it! c++ 완전 정복
    실행 흐름 제어
    이접안
    maritimerobotics
    multithreadedexecutor
    Sliding mode control
    해양공학
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
lidarmansiwon
[ROS2 Multi Thread] Multi Thread의 필요성
상단으로

티스토리툴바