1. 왜 MultiThreadedExecutor가 필요한가요?
ROS 2 노드는 기본적으로 하나의 실행 흐름에서 callback을 처리합니다.
ROS 2 노드는 보통 이렇게 실행합니다.
rclpy.spin(node)
이 코드는 내부적으로 기본 Executor를 사용합니다.
대부분의 기본 동작은 사실상 이런 구조입니다.
executor = SingleThreadedExecutor()
executor.add_node(node)
executor.spin()
즉, callback이 여러 개 있어도 하나의 thread에서 순서대로 처리됩니다.
예를 들어 노드 안에 이런 것들이 있다고 해보겠습니다.
/turtle1/pose subscriber callback
service callback
action execute callback
timer callback
기본 실행에서는 이 callback들이 동시에 실행되지 않습니다.
callback A 실행
끝남
callback B 실행
끝남
callback C 실행
끝남
이런 식입니다.
토픽 구독 callback은 기본적으로는 별도 thread라고 보면 안 됩니다.
토픽 데이터는 DDS 쪽에서 수신되지만, 사용자가 작성한 callback 함수는 Executor가 꺼내서 실행합니다.
예:
def pose_callback(self, msg):
print(msg)
이 함수는 메시지가 왔다고 OS thread가 새로 생겨서 바로 실행되는 게 아닙니다.
Executor가 “실행할 callback이 있네?” 하고 가져와서 실행합니다.
서비스 callback도 마찬가지입니다
def service_callback(self, request, response):
time.sleep(5)
return response
이런 서비스 callback이 있다고 하면, SingleThreadedExecutor에서는 이 5초 동안 다른 callback이 막힙니다.
즉, 그동안:
subscriber callback 실행 안 됨
timer callback 실행 안 됨
다른 service callback 실행 안 됨
action feedback 처리 지연 가능
이런 문제가 생깁니다.
Action Server의 execute_callback은 오래 걸리는 작업을 하는 경우가 많습니다.
예:
def execute_callback(self, goal_handle):
for i in range(10):
time.sleep(1)
goal_handle.publish_feedback(feedback)
goal_handle.succeed()
return result
이 코드는 10초 동안 실행됩니다.
SingleThreadedExecutor에서 이 callback이 실행 중이면, 같은 노드의 다른 callback들이 막힐 수 있습니다.
예를 들어:
Action 실행 중
↓
토픽 구독 callback 지연
서비스 요청 처리 지연
타이머 callback 지연
취소 요청 처리 지연
특히 Action에서 중요한 게 있습니다.
Cancel 요청도 callback입니다.
그런데 execute callback이 thread를 혼자 잡고 있으면 cancel 요청 처리가 늦어질 수 있습니다.
그런데 이번 예제에서는 두 가지 일이 동시에 필요합니다.
Action Server callback 실행
Pose Subscriber callback 실행
Action Server는 목표 거리를 처리하면서 반복문을 계속 실행합니다.
동시에 Subscriber는 /turtle1/pose 토픽을 받아서 현재 위치를 갱신해야 합니다.
만약 단일 스레드로 실행하면 Action Server의 반복문이 실행되는 동안 Subscriber callback이 제때 실행되지 않을 수 있습니다.
그러면 현재 위치가 갱신되지 않고, 이동 거리 계산도 제대로 되지 않습니다.
먼저 멀티스레드의 사용법에 대해서 간략하게 살펴보겠습니다.
2. MultiThreadedExecutor의 역할
그래서 이번 구조에서는 MultiThreadedExecutor를 사용합니다.
from rclpy.executors import MultiThreadedExecutor
MultiThreadedExecutor는 여러 노드의 callback을 병렬적으로 처리할 수 있게 해 줍니다.
즉, Action Server가 동작하는 중에도 Subscriber가 pose 데이터를 계속 받을 수 있습니다.
일반적인 rclpy.spin(node)은 하나의 노드를 계속 실행합니다.
rp.spin(node)
반면 MultiThreadedExecutor는 여러 노드를 등록한 뒤 함께 실행할 수 있습니다.
executor = MultiThreadedExecutor()
executor.add_node(sub)
executor.add_node(pub)
executor.spin()
이 구조에서는 Subscriber 노드와 Publisher 노드가 같은 프로그램 안에서 동시에 동작할 수 있습니다.
3. 예제 파일 구조
예제에서는 다음과 같은 Python 파일을 사용합니다.
my_first_package/
└── my_first_package/
├── my_publisher.py
├── my_subscriber.py
└── my_multi_thread.py
my_publisher.py는 turtlesim에 속도 명령을 보내는 Publisher 역할을 합니다.
my_subscriber.py는 turtlesim의 pose 정보를 읽는 Subscriber 역할을 합니다.
my_multi_thread.py는 두 노드를 동시에 실행하는 실행 파일입니다.

4. my_multi_thread.py 코드 구조
멀티스레드 실행 파일은 대략 다음 구조입니다.
import rclpy as rp
from rclpy.executors import MultiThreadedExecutor
from rclpy.node import Node
from my_first_package.my_publisher import TurtlesimPublisher
from my_first_package.my_subscriber import TurtlesimSubscriber
def main(args=None):
rp.init()
sub = TurtlesimSubscriber()
pub = TurtlesimPublisher()
executor = MultiThreadedExecutor()
executor.add_node(sub)
executor.add_node(pub)
try:
executor.spin()
finally:
executor.shutdown()
sub.destroy_node()
pub.destroy_node()
rp.shutdown()
if __name__ == '__main__':
main()
이 코드는 TurtlesimPublisher와 TurtlesimSubscriber를 각각 객체로 만든 뒤, MultiThreadedExecutor에 등록해서 동시에 실행합니다.

5. import 부분 설명
import rclpy as rp
ROS 2 Python 라이브러리를 불러옵니다.
여기서는 짧게 쓰기 위해 rp라는 별칭을 사용합니다.
from rclpy.executors import MultiThreadedExecutor
멀티스레드 실행기를 사용하기 위한 import입니다.
이번 예제의 핵심입니다.
from rclpy.node import Node
ROS 2 노드를 만들 때 사용하는 기본 클래스입니다.
이 예제에서는 직접 사용하지 않아도 기존 노드 클래스들이 Node를 상속하고 있으므로 함께 이해해 두시면 좋습니다.
from my_first_package.my_publisher import TurtlesimPublisher
from my_first_package.my_subscriber import TurtlesimSubscriber
이전에 작성한 Publisher 클래스와 Subscriber 클래스를 가져옵니다.
같은 패키지 안에 있는 코드를 재사용하는 구조입니다.
6. Publisher와 Subscriber 객체 생성
sub = TurtlesimSubscriber()
pub = TurtlesimPublisher()
여기서 두 개의 노드 객체를 생성합니다.
| sub | /turtle1/pose 토픽을 구독합니다. |
| pub | /turtle1/cmd_vel 토픽을 발행합니다. |
라이브러리로 사용되므로 객체 생성 시 main() 함수가 실행되지 않습니다.
이 두 객체는 각각 독립적인 ROS 2 노드처럼 동작합니다.
7. Executor 생성
executor = MultiThreadedExecutor()
멀티스레드 실행기를 생성합니다.
Executor는 ROS 2에서 callback을 실행해 주는 관리자 역할을 합니다.
8. 노드 등록
executor.add_node(sub)
executor.add_node(pub)
Subscriber 노드와 Publisher 노드를 Executor에 등록합니다.
이 코드의 의미는 다음과 같습니다.
sub 노드의 callback도 실행해 주세요.
pub 노드의 timer callback도 실행해 주세요.
Publisher가 timer를 이용해 주기적으로 /turtle1/cmd_vel을 발행하고, Subscriber가 /turtle1/pose를 계속 수신하려면 두 노드가 모두 Executor에 등록되어야 합니다.
9. Executor 실행
executor.spin()
등록된 노드들의 callback을 계속 실행합니다.
이 코드가 실행되는 동안 프로그램은 종료되지 않고 다음 작업을 반복합니다.
Publisher timer callback 실행
Subscriber topic callback 실행
ROS 이벤트 처리
10. 종료 처리
finally:
executor.shutdown()
pub.destroy_node()
rp.shutdown()
프로그램이 종료될 때 리소스를 정리하는 부분입니다.
executor.shutdown()
Executor를 종료합니다.
pub.destroy_node()
Publisher 노드를 제거합니다.
rp.shutdown()
ROS 2 Python 시스템을 종료합니다.
sub.destroy_node()도 추가합니다.
finally:
executor.shutdown()
sub.destroy_node()
pub.destroy_node()
rp.shutdown()
11. setup.py에 실행 파일 등록하기
Python 파일을 만들었다고 해서 바로 ros2 run으로 실행할 수 있는 것은 아닙니다.
setup.py의 entry_points에 실행 명령을 등록해야 합니다.
예시는 다음과 같습니다.
entry_points={
'console_scripts': [
'my_multi_thread = my_first_package.my_multi_thread:main',
],
},

이 설정의 의미는 다음과 같습니다.
ros2 run my_first_package my_multi_thread
명령을 실행하면,
my_first_package/my_multi_thread.py 파일의 main() 함수 실행
이라는 뜻입니다.
12. 빌드 및 환경 적용
setup.py를 수정했다면 반드시 빌드해야 합니다.
cd ~/ros2_study
colcon build

빌드 후 환경을 다시 적용합니다.
source install/setup.bash
또는
sl

13. 실행 방법
첫 번째 터미널에서 turtlesim을 실행합니다.
ros2 run turtlesim turtlesim_node

두 번째 터미널에서 멀티스레드 예제를 실행합니다.
ros2 run my_first_package my_multi_thread
정상적으로 실행되면 거북이가 움직이고, 터미널에는 pose 값이 출력됩니다.

14. 실행 결과 해석
Publisher는 /turtle1/cmd_vel로 속도 명령을 발행합니다.
예를 들어 Publisher 코드가 다음과 같이 작성되어 있다면,
msg.linear.x = 2.0
msg.angular.z = 2.0
self.publisher.publish(msg)
거북이는 앞으로 이동하면서 동시에 회전합니다.
그래서 화면에는 원형 또는 곡선 형태의 궤적이 나타납니다.
Subscriber는 /turtle1/pose를 구독해서 현재 위치를 출력합니다.
X: ...
Y: ...
이 값은 거북이가 움직이면서 계속 변합니다.
15. rqt_graph로 연결 확인
실행 중 다른 터미널에서 다음 명령을 실행합니다.
rqt_graph
정상적으로 실행 중이라면 다음 구조를 볼 수 있습니다.
/turtlesim_publisher → /turtle1/cmd_vel → /turtlesim
/turtlesim → /turtle1/pose → /turtlesim_subscriber
이 그래프를 보면 Publisher, Subscriber, turtlesim 노드가 어떤 토픽으로 연결되어 있는지 확인할 수 있습니다.

16. 이 예제가 Action Server와 연결되는 이유
이 멀티스레드 예제는 단순히 Publisher와 Subscriber를 동시에 실행하는 연습처럼 보일 수 있습니다.
하지만 다음 단계인 Action Server 구현에서 매우 중요합니다.
Action Server로 거북이를 지정 거리만큼 이동시키려면 두 가지 작업이 동시에 필요합니다.
1. /turtle1/cmd_vel 발행
2. /turtle1/pose 구독
Action Server의 execute_callback()이 실행되는 동안에도 pose 정보가 계속 들어와야 합니다.
그래야 현재 위치를 기준으로 이동 거리를 계산할 수 있습니다.
따라서 다음 구조가 필요합니다.
Action Server callback 실행 중
+
Pose Subscriber callback 계속 실행
이때 MultiThreadedExecutor를 사용하면 두 callback을 동시에 처리할 수 있습니다.
17. 단일 스레드와 멀티스레드 비교
| 실행 방식 | 하나의 callback씩 순차 처리 | 여러 callback 처리 가능 |
| 구조 | 단순함 | 조금 복잡함 |
| 장점 | 이해하기 쉬움 | 동시 작업에 유리함 |
| 단점 | 긴 callback에 취약함 | 상태 공유 주의 필요 |
| 사용 예 | 간단한 Publisher, Subscriber | Action, 여러 센서, 제어 루프 |
18. 실무 관점에서 주의할 점
멀티스레드를 사용하면 여러 callback이 동시에 실행될 수 있습니다.
그래서 하나의 변수를 여러 callback에서 동시에 읽고 쓰는 경우 주의해야 합니다.
예를 들어 다음과 같은 변수는 공유 변수입니다.
self.current_pose
한 callback에서는 값을 업데이트하고, 다른 callback에서는 값을 읽을 수 있습니다.
학습용 예제에서는 큰 문제가 없지만, 실제 로봇에서는 데이터 동기화 문제가 생길 수 있습니다.
실무에서는 다음 방법을 고려합니다.
1. Lock 사용
2. CallbackGroup 분리
3. 상태 변수를 최소화
4. 메시지 기반 구조로 재설계
5. Timer callback으로 제어 루프 분리
19. 정리
이번 글에서는 ROS 2의 MultiThreadedExecutor를 사용해 Publisher와 Subscriber를 동시에 실행하는 방법을 정리했습니다.
이 구조는 단순한 병렬 실행 예제가 아니라, 실제 로봇 제어에서 매우 중요한 기본 패턴입니다.
로봇을 움직이려면 명령을 보내야 하고, 동시에 현재 상태를 읽어야 합니다.
즉, 제어 명령과 센서 피드백이 함께 동작해야 합니다.
이번 예제에서는 /turtle1/cmd_vel을 발행하는 Publisher와 /turtle1/pose를 구독하는 Subscriber를 하나의 MultiThreadedExecutor에 등록했습니다. 이를 통해 거북이를 움직이면서 동시에 위치 정보를 확인할 수 있었습니다.
다음 단계에서는 이 구조를 Action Server와 결합하여, 사용자가 지정한 거리만큼 거북이를 이동시키고 남은 거리를 Feedback으로 반환하는 기능을 구현할 수 있습니다.
'강좌 > ROS2' 카테고리의 다른 글
| 소스에서 파라미터 사용하기 (0) | 2026.05.10 |
|---|---|
| ROS 2 MultiThreadedExecutor와 Action Server로 Turtlesim 거리 이동 구현하기 (0) | 2026.05.10 |
| ROS2 Action Server 만들기 #2 (0) | 2026.05.10 |
| ROS 2 Action 정의 만들기 (0) | 2026.05.10 |
| ROS2 사용자 정의 메세지 만들기 #3 (0) | 2026.05.09 |