학습 목표

  • Executor가 Subscription·Timer·Service·Action Callback을 실행하는 과정을 설명할 수 있다.
  • SingleThreadedExecutor와 MultiThreadedExecutor를 상황에 맞게 선택할 수 있다.
  • MutuallyExclusive와 Reentrant Callback Group의 차이를 실제 병렬 실행 조건으로 설명할 수 있다.
  • 동기 Service 호출의 Deadlock, Race Condition과 Lock 경합을 예방할 수 있다.
  • Python과 C++ Node에 Callback Group을 적용하고 지연을 측정할 수 있다.
  • 실제 Robot의 Control·Sensor·UI·Action Callback을 안전하게 분리할 수 있다.

1. Executor는 Callback 실행 관리자다

Node가 생성됐다고 Callback이 저절로 실행되지는 않습니다. Executor는 운영체제 Thread를 사용해 준비된 ROS Entity의 Callback을 호출합니다.

  • Subscription: Message가 도착하면 Callback 준비
  • Timer: 설정한 시간이 되면 Callback 준비
  • Service Server: Request가 들어오면 Callback 준비
  • Service·Action Client: Response·Feedback·Result와 Future 완료 처리
  • Guard Condition·Waitable: 내부 Event가 준비되면 처리

spin()은 처리할 일이 생길 때까지 기다리고, 실행 가능한 Callback을 선택해 호출하는 과정을 반복합니다. ROS 2 Client Library는 일반적으로 별도의 무제한 Message Queue를 하나 더 만들지 않고 Middleware에 보관된 Data를 가져옵니다. Wait Set은 Entity별 준비 여부를 Executor에 알립니다.

01Middleware에 Data·Event 도착
02Wait Set이 준비된 Entity를 표시
03Executor가 실행 가능한 Callback 선택
04Callback Group 규칙 확인
05가용 Thread에서 Callback 실행
06다시 대기

준비되었다는 표시는 Message 개수 전체를 뜻하는 Queue 길이가 아닙니다. 정확한 처리 순서와 우선순위가 필요하면 기본 Executor의 우연한 순서에 의존하지 말아야 합니다.


2. spin 계열 API 이해하기

가장 단순한 Python 실행은 다음과 같습니다.

rclpy.init()
node = MyNode()
rclpy.spin(node)  # 기본 Executor를 이용해 종료될 때까지 처리
node.destroy_node()
rclpy.shutdown()

명시적 Executor를 사용하면 Node 수, Thread 수와 종료 절차를 직접 관리할 수 있습니다.

executor = rclpy.executors.SingleThreadedExecutor()
executor.add_node(node)
try:
    executor.spin()
finally:
    executor.shutdown()

주요 API는 다음처럼 구분합니다.

API 용도 주의점
spin() 종료까지 계속 처리 현재 Thread를 점유
spin_once() 준비된 작업을 한 차례 처리 같은 Executor에서 동시에 호출하지 않음
spin_until_future_complete() Future 완료까지 처리 Callback 안에서 잘못 사용하면 Deadlock 가능
add_node() Executor에 Node 등록 한 Node를 여러 Executor에 중복 등록하지 않음
remove_node() Node 제거 진행 중 Callback과 종료 순서를 설계
shutdown() Executor Thread 종료 Node·Context 정리 전에 호출

3. SingleThreadedExecutor: 가장 단순한 기준점

SingleThreadedExecutor는 한 Thread에서 Callback을 하나씩 실행합니다. 공유 상태에 동시 접근하는 Callback이 없으므로 추론과 재현이 쉽습니다. 모든 Callback이 짧고 주기 요구가 느슨한 Node라면 좋은 기본 선택입니다.

#!/usr/bin/env python3
import time
import rclpy
from rclpy.node import Node

class BlockingDemo(Node):
    def __init__(self):
        super().__init__('blocking_demo')
        self.last_fast = time.perf_counter()
        self.create_timer(0.5, self.slow_callback)
        self.create_timer(0.1, self.fast_callback)

    def slow_callback(self):
        self.get_logger().info('slow start')
        time.sleep(0.4)  # I/O 대기를 흉내 낸다. 실제 제어 Callback에서는 금지한다.
        self.get_logger().info('slow end')

    def fast_callback(self):
        now = time.perf_counter()
        interval_ms = (now - self.last_fast) * 1000.0
        self.last_fast = now
        self.get_logger().info(f'fast interval={interval_ms:.1f} ms')

def main():
    rclpy.init()
    node = BlockingDemo()
    executor = rclpy.executors.SingleThreadedExecutor()
    executor.add_node(node)
    try:
        executor.spin()
    except KeyboardInterrupt:
        pass
    finally:
        executor.shutdown()
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

fast interval이 약 100 ms에서 크게 벌어지는 순간을 관찰합니다. Timer가 과부하 상태에서 정확히 몇 번 실행되는지는 Client Library·Executor와 시점에 따라 달라질 수 있으므로 “밀린 횟수를 반드시 모두 재실행한다” 또는 “항상 모두 버린다”고 가정하지 않습니다.


4. MultiThreadedExecutor가 해결하는 문제

MultiThreadedExecutor는 Thread Pool을 두고 Callback Group 규칙이 허용하는 여러 Callback을 병렬 실행합니다. 긴 I/O Callback 때문에 독립적인 Control Timer가 기다리는 상황을 줄일 수 있습니다.

executor = rclpy.executors.MultiThreadedExecutor(num_threads=4)
executor.add_node(node)
executor.spin()

그러나 다음 조건이 모두 맞아야 실제 병렬 실행이 가능합니다.

  1. 동시에 실행할 Callback이 둘 이상 준비되어 있다.
  2. 서로 다른 Callback Group이거나 Reentrant Group이 병렬 실행을 허용한다.
  3. 사용 가능한 Worker Thread가 있다.
  4. 같은 Lock, Python GIL, CPU 포화나 외부 Resource가 직렬화하지 않는다.
기대 실제 확인할 조건
Thread를 늘리면 빨라진다 CPU 작업인지 I/O 대기인지 측정
모든 Callback이 병렬로 돈다 Callback Group 배치 확인
Reentrant면 Thread-safe다 공유 상태 보호는 개발자 책임
평균 Hz가 좋으면 안전하다 최악 지연과 Deadline Miss 확인

5. Callback Group이 병렬 실행 규칙을 정한다

Node의 Entity는 Callback Group에 속합니다. Group을 지정하지 않으면 기본 MutuallyExclusiveCallbackGroup에 들어갑니다.

MutuallyExclusiveCallbackGroup

같은 Group의 Callback은 동시에 실행되지 않습니다. 공유 상태를 별도 Lock 없이 직렬 접근하게 만들 수 있지만, 긴 Callback 하나가 같은 Group의 모든 Callback을 막습니다.

ReentrantCallbackGroup

같은 Group의 서로 다른 Callback뿐 아니라 같은 Callback의 중첩 실행도 허용할 수 있습니다. “자동 Thread-safe”라는 뜻은 아닙니다.

서로 다른 Group

Group 종류와 무관하게 서로 다른 Group의 Callback은 MultiThreadedExecutor에서 병렬 실행될 수 있습니다. 두 개의 MutuallyExclusive Group은 각 Group 내부에서는 하나씩, Group 사이는 동시에 실행될 수 있습니다.

배치 같은 Callback 동시 실행 서로 다른 Callback 동시 실행
하나의 기본 MutuallyExclusive Group 불가 불가
하나의 Reentrant Group 가능 가능
서로 다른 MutuallyExclusive Group 각 Group 내부 불가 Group 사이 가능

생성한 Callback Group 객체는 Node가 실행되는 동안 멤버로 보관해야 합니다. 지역 변수로만 만들고 수명이 끝나면 Callback이 정상적으로 Trigger되지 않을 수 있습니다.


6. Python에서 Group을 분리하는 전체 예제

#!/usr/bin/env python3
import threading
import time
import math
import rclpy
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup
from rclpy.executors import MultiThreadedExecutor
from rclpy.node import Node
from geometry_msgs.msg import Twist
from sensor_msgs.msg import LaserScan
from std_srvs.srv import Trigger

class RobotExecutorNode(Node):
    def __init__(self):
        super().__init__('robot_executor_node')
        self.sensor_group = MutuallyExclusiveCallbackGroup()
        self.control_group = MutuallyExclusiveCallbackGroup()
        self.service_group = MutuallyExclusiveCallbackGroup()

        self.state_lock = threading.Lock()
        self.nearest = math.inf
        self.last_scan_ns = 0

        self.cmd_pub = self.create_publisher(Twist, '/lab/cmd_vel', 10)
        self.scan_sub = self.create_subscription(
            LaserScan, '/scan', self.on_scan, 10,
            callback_group=self.sensor_group)
        self.control_timer = self.create_timer(
            0.05, self.control, callback_group=self.control_group)
        self.service = self.create_service(
            Trigger, '~/status', self.status,
            callback_group=self.service_group)

    def on_scan(self, msg):
        valid = [x for x in msg.ranges if math.isfinite(x) and x > 0.0]
        nearest = min(valid) if valid else math.inf
        with self.state_lock:
            self.nearest = nearest
            self.last_scan_ns = self.get_clock().now().nanoseconds

    def control(self):
        now_ns = self.get_clock().now().nanoseconds
        with self.state_lock:  # 값만 Snapshot하고 즉시 해제한다.
            nearest = self.nearest
            age_ms = (now_ns - self.last_scan_ns) / 1e6

        cmd = Twist()
        if age_ms <= 250.0 and nearest >= 0.5:
            cmd.linear.x = 0.2
        self.cmd_pub.publish(cmd)  # Lock 밖에서 Publish한다.

    def status(self, request, response):
        del request
        with self.state_lock:
            response.message = f'nearest={self.nearest:.3f} m'
        response.success = True
        return response

def main():
    rclpy.init()
    node = RobotExecutorNode()
    executor = MultiThreadedExecutor(num_threads=3)
    executor.add_node(node)
    try:
        executor.spin()
    except KeyboardInterrupt:
        pass
    finally:
        stop = Twist()
        node.cmd_pub.publish(stop)
        executor.shutdown()
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

이 예제는 Group을 세 개로 분리하지만 공유 상태에는 Lock이 필요합니다. Control Callback은 오래된 Scan이면 정지 명령을 내립니다. 실물에서는 /lab/cmd_vel을 검증한 뒤 Mux·Watchdog·E-stop이 있는 Command 경로에 연결합니다.


7. C++에서 Callback Group 적용하기

#include <chrono>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;

class GroupDemo : public rclcpp::Node {
public:
  GroupDemo() : Node("group_demo") {
    sensor_group_ = create_callback_group(
      rclcpp::CallbackGroupType::MutuallyExclusive);
    control_group_ = create_callback_group(
      rclcpp::CallbackGroupType::MutuallyExclusive);

    rclcpp::SubscriptionOptions options;
    options.callback_group = sensor_group_;
    sub_ = create_subscription<std_msgs::msg::String>(
      "input", 10,
      [this](std_msgs::msg::String::ConstSharedPtr msg) {
        std::lock_guard<std::mutex> guard(mutex_);
        latest_ = msg->data;
      }, options);

    timer_ = create_wall_timer(20ms, [this]() {
      std::string snapshot;
      {
        std::lock_guard<std::mutex> guard(mutex_);
        snapshot = latest_;
      }
      RCLCPP_INFO_THROTTLE(get_logger(), *get_clock(), 2000,
                           "latest=%s", snapshot.c_str());
    }, control_group_);
  }

private:
  rclcpp::CallbackGroup::SharedPtr sensor_group_, control_group_;
  rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_;
  rclcpp::TimerBase::SharedPtr timer_;
  std::mutex mutex_;
  std::string latest_;
};

int main(int argc, char ** argv) {
  rclcpp::init(argc, argv);
  auto node = std::make_shared<GroupDemo>();
  rclcpp::executors::MultiThreadedExecutor executor(
    rclcpp::ExecutorOptions(), 2);
  executor.add_node(node);
  executor.spin();
  rclcpp::shutdown();
}

C++ Lambda에서 this를 Capture할 때 Node보다 Callback이 오래 살아남지 않도록 종료 순서를 관리합니다. Mutex를 잡은 상태로 Log·Publish·Service 호출을 하지 않는 것이 좋습니다.


8. 가장 위험한 함정: Callback 안의 동기 호출

Timer Callback이 동기 Service 호출 결과를 기다리고, 그 Future의 완료 Callback이 같은 MutuallyExclusive Group에 있다면 서로 영원히 기다릴 수 있습니다.

01Timer Callback이 Group을 점유
02동기 Service 호출 후 Future 완료를 기다림
03Response 도착
04Future 완료 Callback도 같은 Group 실행 권한이 필요
05Timer가 Group을 놓지 않아 Deadlock

가장 안전한 해법은 비동기 호출 후 Done Callback에서 결과를 처리하는 것입니다.

def request_async(self):
    if not self.client.service_is_ready():
        self.get_logger().warn('service not ready')
        return
    future = self.client.call_async(Trigger.Request())
    future.add_done_callback(self.on_response)

def on_response(self, future):
    try:
        response = future.result()
        self.get_logger().info(response.message)
    except Exception as exc:
        self.get_logger().error(f'service failed: {exc}')

동기 호출이 꼭 필요하면 호출 Callback과 Client를 서로 다른 Group에 두거나 Reentrant Group을 사용하고 MultiThreadedExecutor에 가용 Thread가 있어야 합니다. 그래도 Timeout·취소·종료 처리가 쉬운 비동기 구조를 우선합니다.


9. Race Condition과 Lock 설계

서로 다른 Group이 병렬 실행되면 여러 Field의 관계가 깨질 수 있습니다. 예를 들어 posestamp를 따로 갱신하는 중 Control Callback이 읽으면 서로 다른 시점의 값이 섞입니다.

안전한 Pattern은 다음과 같습니다.

  1. 공유 상태를 하나의 불변 Snapshot으로 묶는다.
  2. Lock 안에서는 복사·교체만 수행한다.
  3. 계산, File I/O, Network, Publish와 Service 호출은 Lock 밖에서 한다.
  4. Lock 획득 순서를 고정하고 중첩 Lock을 피한다.
  5. 종료 Flag와 Callback 수명도 같은 동기화 규칙으로 보호한다.
with self.lock:
    snapshot = (self.pose, self.stamp_ns, self.valid)

# 긴 계산은 Lock 밖에서
command = compute_command(*snapshot)
self.publisher.publish(command)

Python에서는 GIL이 모든 Race Condition을 없애주지 않습니다. 복합 연산, C Extension, I/O 중 GIL 해제와 여러 Field의 일관성 문제 때문에 명시적 동기화가 필요합니다.


10. Action·Service·Client의 숨은 Callback

Callback Group은 사용자가 직접 작성한 함수에만 적용되지 않습니다. Service Response, Action Goal·Cancel·Feedback·Result와 Future Done Callback 같은 내부 Callback도 Entity의 Group 규칙을 따릅니다.

Entity 고려할 Callback 권장 설계
Service Client Response·Future Done 비동기 호출, 호출자와 진행 가능성 보장
Action Client Goal Response·Feedback·Result 긴 사용자 처리 금지
Action Server Goal·Cancel·Execute Cancel이 Execute 중에도 처리 가능한 구조
Timer 주기 Callback Blocking I/O 금지, 실제 간격 측정
Subscription Message Callback 최신값 Snapshot과 무거운 계산 분리

Action Execute Callback이 오래 걸린다면 Cancel Callback이 실행될 Thread와 Group을 확보해야 합니다. 단, Reentrant만 지정했다고 안전 정지가 보장되는 것은 아닙니다. Execute Loop가 Cancel 요청을 주기적으로 확인하고 Hardware Stop을 수행해야 합니다.


11. CPU-bound와 I/O-bound를 구분한다

MultiThreading 효과는 작업 성격에 따라 다릅니다.

  • I/O-bound: Device·Network·Disk 대기가 길면 다른 Callback이 실행될 여지가 큼
  • CPU-bound C++: 여러 Core 활용 가능하지만 공유 Cache와 Lock 경합 측정 필요
  • CPU-bound Python: GIL 때문에 순수 Python 계산 병렬성이 제한될 수 있음
  • GPU·C Extension: GIL 해제 여부와 비동기 완료 방식 확인

Python의 무거운 영상·경로 계산은 별도 Process, C++ Node, Worker Queue 또는 가속 Library를 검토합니다. Callback에서 무제한 Thread를 새로 만드는 것은 종료·Backpressure·Memory 관리 문제를 만듭니다.


12. 실제 Robot Callback 배치 예

기능 Group 예 이유
50 Hz Control Timer 전용 MutuallyExclusive 같은 Control Callback 중첩 방지
Scan·IMU 상태 갱신 Sensor MutuallyExclusive Sensor 상태 갱신 직렬화
UI·진단 Service 별도 MutuallyExclusive Control과 독립 처리
장시간 Action 별도 Group Cancel·Feedback 진행성 확보
Parameter Callback Configuration Group 설정 변경과 Control 상태 동기화

중요한 Control Callback을 Group만 나눈다고 운영체제 Scheduling 우선순위가 자동으로 높아지지는 않습니다. Hard Real-time 요구가 있으면 WCET, Memory Allocation, Priority Inversion, OS RT Scheduling, rclcpp WaitSet·rclc Executor 같은 별도 설계를 검토해야 합니다.


13. 지연을 숫자로 측정하는 계측 코드

def control_callback(self):
    started = time.perf_counter_ns()
    interval_ms = (started - self.last_started_ns) / 1e6
    self.last_started_ns = started

    self.run_control()

    elapsed_ms = (time.perf_counter_ns() - started) / 1e6
    if elapsed_ms > 10.0 or interval_ms > 60.0:
        self.get_logger().warn(
            f'control runtime={elapsed_ms:.2f} ms interval={interval_ms:.2f} ms',
            throttle_duration_sec=2.0)

측정 항목을 구분합니다.

  • Callback 실행 시간: 시작부터 종료까지
  • 시작 간격: 연속 실행 시작 시각 차이
  • Message Age: Header Stamp부터 처리 시각까지
  • Queue·전송 영향: Publisher와 Subscriber에서 관측 비교
  • 최악값과 Percentile: 평균 외에 max, p95, p99
  • Deadline Miss와 안전 반응: Watchdog가 실제로 정지시키는지
ros2 topic hz /control/heartbeat
ros2 topic delay /scan
ros2 topic info /scan --verbose
top -H -p <PID>
pidstat -t -p <PID> 1

ros2 topic hz는 CLI Subscription이 받은 표본 기준이므로 Callback 내부 시간 계측과 함께 사용합니다.


14. 단계별 실험: Group 효과 확인

  1. BlockingDemo를 SingleThreadedExecutor로 실행합니다.
  2. Fast Timer의 최대 시작 간격을 30초 기록합니다.
  3. Executor만 MultiThreaded로 바꾸고 Group은 기본값으로 둡니다.
  4. 결과가 거의 같은 이유를 설명합니다.
  5. 두 Timer를 서로 다른 MutuallyExclusive Group에 넣습니다.
  6. Thread 1·2·4개 조건에서 다시 측정합니다.
  7. Slow Callback을 CPU 계산과 sleep 두 종류로 비교합니다.
  8. 평균뿐 아니라 max·p95와 CPU 사용량을 표로 남깁니다.
ros2 run my_executor_demo blocking_demo --ros-args --log-level info
top -H -p $(pgrep -n -f blocking_demo)

실험 중 실제 Robot Command Topic을 사용하지 말고 격리 Namespace나 Simulator를 사용합니다.


15. 증상별 진단표

증상 가능성이 높은 원인 확인·대응
MultiThreaded인데 하나씩 실행 모두 기본 Group Group 객체와 Entity 지정 확인
Service 호출에서 멈춤 같은 MutuallyExclusive Group의 동기 호출 비동기 호출 또는 Group 분리
가끔 상태가 모순됨 Race Condition Snapshot·Lock·Thread Sanitizer 검토
Thread를 늘려도 느림 CPU 포화·GIL·Lock 경합 Thread별 CPU와 Lock 구간 측정
Control 주기가 간헐적으로 큼 긴 Callback·OS Scheduling 실행 시간·시작 간격·CPU 확인
Action Cancel이 늦음 Execute가 Thread·Group 점유 Cancel 진행성 및 Loop 확인 주기 설계
종료 시 Hang Worker·Future·Lock 미정리 Timeout, Cancel, Executor 종료 순서 확인
Message가 빠짐 처리율 부족·QoS·Network Age·Hz·QoS·Publisher/Subscriber 비교

16. Executor 선택 절차

01모든 Callback이 짧고 Deadline이 느슨함
02SingleThreadedExecutor로 시작
03실행 시간·간격·Message Age 측정
04긴 작업을 State Machine·비동기 I/O·별도 Node로 분리 가능한지 검토
05독립 Callback의 진행성이 필요하면 Group 분리
06MultiThreadedExecutor 적용 후 Race·Deadlock·CPU 재시험

Thread 수는 “Group 수 이상” 같은 공식으로 정하지 않습니다. 동시에 준비되는 작업 수, Core 수, Blocking 특성, GIL, WCET와 CPU 예산을 측정해 결정합니다. 1부터 늘리며 Worst-case가 개선되지 않는 지점을 찾습니다.


17. Composition과 고급 실행 모델

여러 C++ Component를 한 Process에 넣으면 Executor와 Thread를 공유합니다. 한 Component의 긴 Callback이 다른 Component에 영향을 줄 수 있으므로 Container Executor 종류와 Callback Group을 함께 설계합니다.

# Single-threaded component container
ros2 run rclcpp_components component_container

# 지원 배포판에서 Multi-threaded container와 Thread 수 지정
ros2 run rclcpp_components component_container \
  --executor-type multi-threaded --ros-args -p thread_num:=4

최신 ROS 2 Rolling에는 Event Queue 기반 EventsCBGExecutor가 문서화되어 있습니다. 이는 모든 배포판에 동일하게 존재하지 않으므로 설치한 Distribution·rclcpp Version의 API를 확인해야 합니다. 결정적 실행 순서나 엄격한 Real-time이 필요하면 rclcpp::WaitSet, rclc Executor와 OS Scheduling을 별도 검토합니다.


18. 운영 전 Checklist

  • 모든 Entity의 Callback Group을 표로 문서화했다.
  • Group 객체를 Node 수명 동안 보관한다.
  • Callback 안의 동기 Service·Action 호출을 제거하거나 진행성을 증명했다.
  • Lock 안에서 Blocking I/O·Publish·Service 호출을 하지 않는다.
  • Callback 실행 시간, 시작 간격과 Message Age의 한계를 정했다.
  • 평균뿐 아니라 p95·p99·최악값을 실제 부하에서 측정했다.
  • Sensor 단절·Service Timeout·Action Cancel·종료를 시험했다.
  • 오래된 Sensor Data와 Command를 Watchdog가 거부한다.
  • CPU 포화와 Thread 수 변화 A/B Test를 수행했다.
  • 실제 Robot Test 전 Simulator·격리 Topic·E-stop을 확인했다.

19. 확인 퀴즈 15문항

답을 선택한 뒤 정답 확인을 누르세요. 정답과 해설은 제출 후에 표시됩니다.

1. Executor의 핵심 역할은?

2. 기본 Callback Group의 종류는?

3. MultiThreadedExecutor로 바꿔도 병렬 실행되지 않는 대표 이유는?

4. Reentrant Group의 의미는?

5. 서로 다른 두 MutuallyExclusive Group은 MultiThreadedExecutor에서 어떻게 되는가?

6. Callback Group 객체를 Node 멤버로 보관하는 이유는?

7. 같은 MutuallyExclusive Group에서 동기 Service 호출이 Deadlock을 만드는 이유는?

8. 가장 안전한 Service Client 호출 Pattern은?

9. Lock 사용 원칙으로 알맞은 것은?

10. Python GIL에 대한 올바른 설명은?

11. 평균 20 Hz만으로 Control Timing이 안전하다고 할 수 없는 이유는?

12. Thread를 늘려도 처리량이 개선되지 않는 경우는?

13. Action Cancel 진행성을 위해 필요한 것은?

14. Hard Real-time 요구에서 기본 Executor만으로 부족할 수 있는 이유는?

15. Executor를 바꾸기 전에 먼저 할 일은?


ROBOT GLOSSARY

용어 정리

전체 용어 찾아보기 →
Executor콜백 실행 관리자
준비된 ROS Entity의 Callback을 하나 이상의 운영체제 Thread에서 실행하는 구성요소입니다.
Callback콜백 함수
Message, Timer, Service, Action 등 특정 Event가 준비될 때 Executor가 호출하는 함수입니다.
Wait Set대기 집합
Subscription, Timer 등 Entity의 준비 상태를 Executor에 알리는 저수준 대기 구조입니다.
SingleThreadedExecutor단일 스레드 실행기
한 Thread에서 Callback을 하나씩 실행하는 단순하고 추론하기 쉬운 Executor입니다.
MultiThreadedExecutor다중 스레드 실행기
Thread Pool을 사용해 Callback Group 규칙이 허용하는 Callback을 병렬 처리하는 Executor입니다.
Callback Group콜백 실행 그룹
Node 안의 Callback을 묶어 서로 병렬 실행할 수 있는지를 정하는 단위입니다.
MutuallyExclusive상호 배제 그룹
같은 Group에 속한 Callback을 한 번에 하나만 실행하도록 제한하는 Callback Group입니다.
Reentrant재진입 그룹
같은 Group의 Callback과 같은 Callback의 중첩·병렬 실행을 허용할 수 있는 Group입니다.
Deadlock교착상태
둘 이상의 작업이 상대가 Resource를 놓기를 기다리며 영원히 진행하지 못하는 상태입니다.
Race Condition경쟁 상태
여러 Thread의 실행 순서에 따라 공유 상태의 결과가 달라지는 결함입니다.
Starvation기아 상태
다른 작업이 Resource를 계속 차지해 특정 Callback이 장시간 실행 기회를 얻지 못하는 상태입니다.
Future비동기 결과 객체
Service·Action 등 아직 완료되지 않은 작업의 이후 결과와 완료 Callback을 나타내는 객체입니다.
Thread Pool스레드 풀
작업을 반복 처리하도록 미리 준비한 Worker Thread 집합입니다.
Critical Section임계 구역
공유 상태의 일관성을 위해 한 Thread만 접근하도록 Lock으로 보호하는 Code 구간입니다.
GIL파이썬 전역 인터프리터 잠금
CPython Bytecode 실행을 제한하며 순수 Python CPU 병렬성에 영향을 주는 Interpreter Lock입니다.
Deadline Miss마감시간 위반
Callback이나 Data 처리가 요구된 시간 한계 안에 완료되지 못한 상태입니다.
Priority Inversion우선순위 역전
높은 우선순위 작업이 낮은 우선순위 작업이 가진 Resource 때문에 지연되는 현상입니다.
WCET최악 실행 시간
정해진 조건에서 작업이 완료되는 데 걸릴 수 있는 최대 실행 시간의 분석값입니다.
Backpressure역압
Consumer가 처리 가능한 속도에 맞게 Producer나 Queue 유입을 제한하는 흐름 제어입니다.
EventsCBGExecutor이벤트 기반 콜백 그룹 실행기
지원되는 최신 ROS 2에서 FIFO Event Queue와 Callback Group을 이용하는 rclcpp Executor입니다.

연습 문제

  1. 자신의 Node에서 Subscription·Timer·Service·Action Callback 목록과 Group 배치표를 작성하세요.
  2. SingleThreaded BlockingDemo의 fast Timer 간격 max·p95를 측정하세요.
  3. Executor만 MultiThreaded로 바꾸었을 때 결과가 같은 이유를 설명하세요.
  4. 두 Timer를 별도 MutuallyExclusive Group으로 분리하고 다시 측정하세요.
  5. Thread 1·2·4·8개 조건에서 CPU와 최악 지연을 비교하세요.
  6. sleep 기반 I/O 흉내와 순수 Python CPU 계산의 차이를 비교하세요.
  7. 같은 기본 Group의 동기 Service Deadlock을 격리 환경에서 재현하세요.
  8. 위 Service Client를 call_async와 Done Callback 구조로 수정하세요.
  9. Pose·Timestamp의 Race Condition Test와 Snapshot 해법을 구현하세요.
  10. Lock 안에서 Publish하는 Code를 찾아 Critical Section을 줄이세요.
  11. Action Execute 중 Cancel이 100 ms 이내 처리되는지 시험하세요.
  12. Scan Message Age가 250 ms를 넘으면 정지하는 Watchdog를 구현하세요.
  13. Python과 C++ MultiThreaded 예제의 CPU-bound 결과를 비교하세요.
  14. Component Container의 Executor Type·Thread 수를 바꾸어 측정하세요.
  15. 실제 Robot 적용 전 Failure Injection·E-stop·Rollback Checklist를 작성하세요.

참고 자료