학습 목표
- 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에 알립니다.
준비되었다는 표시는 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()
그러나 다음 조건이 모두 맞아야 실제 병렬 실행이 가능합니다.
- 동시에 실행할 Callback이 둘 이상 준비되어 있다.
- 서로 다른 Callback Group이거나 Reentrant Group이 병렬 실행을 허용한다.
- 사용 가능한 Worker Thread가 있다.
- 같은 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에 있다면 서로 영원히 기다릴 수 있습니다.
가장 안전한 해법은 비동기 호출 후 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의 관계가 깨질 수 있습니다. 예를 들어 pose와 stamp를 따로 갱신하는 중 Control Callback이 읽으면 서로 다른 시점의 값이 섞입니다.
안전한 Pattern은 다음과 같습니다.
- 공유 상태를 하나의 불변 Snapshot으로 묶는다.
- Lock 안에서는 복사·교체만 수행한다.
- 계산, File I/O, Network, Publish와 Service 호출은 Lock 밖에서 한다.
- Lock 획득 순서를 고정하고 중첩 Lock을 피한다.
- 종료 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 효과 확인
- BlockingDemo를 SingleThreadedExecutor로 실행합니다.
- Fast Timer의 최대 시작 간격을 30초 기록합니다.
- Executor만 MultiThreaded로 바꾸고 Group은 기본값으로 둡니다.
- 결과가 거의 같은 이유를 설명합니다.
- 두 Timer를 서로 다른 MutuallyExclusive Group에 넣습니다.
- Thread 1·2·4개 조건에서 다시 측정합니다.
- Slow Callback을 CPU 계산과
sleep두 종류로 비교합니다. - 평균뿐 아니라 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 선택 절차
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의 핵심 역할은?
Executor는 Wait Set 등을 통해 준비 상태를 확인하고 Callback 실행을 관리합니다.
2. 기본 Callback Group의 종류는?
Group을 지정하지 않은 Entity는 Node의 기본 MutuallyExclusive Group에 들어갑니다.
3. MultiThreadedExecutor로 바꿔도 병렬 실행되지 않는 대표 이유는?
같은 MutuallyExclusive Group의 Callback은 동시에 실행될 수 없습니다.
4. Reentrant Group의 의미는?
동시 실행을 허용할 뿐 공유 상태 보호는 개발자의 책임입니다.
5. 서로 다른 두 MutuallyExclusive Group은 MultiThreadedExecutor에서 어떻게 되는가?
상호 배제는 각 Group 내부에 적용됩니다.
6. Callback Group 객체를 Node 멤버로 보관하는 이유는?
Group 객체가 사라지지 않도록 Entity와 같은 수명으로 관리합니다.
7. 같은 MutuallyExclusive Group에서 동기 Service 호출이 Deadlock을 만드는 이유는?
Response가 도착해도 완료 처리가 실행 권한을 얻지 못합니다.
8. 가장 안전한 Service Client 호출 Pattern은?
비동기 구조는 Executor가 다른 Callback을 처리할 진행성을 확보하기 쉽습니다.
9. Lock 사용 원칙으로 알맞은 것은?
Critical Section을 짧게 유지해야 경합과 지연을 줄일 수 있습니다.
10. Python GIL에 대한 올바른 설명은?
I/O와 C Extension은 GIL을 해제할 수 있고 복합 상태는 별도 보호가 필요합니다.
11. 평균 20 Hz만으로 Control Timing이 안전하다고 할 수 없는 이유는?
max·p95·p99와 Callback 내부 시작 간격을 함께 측정합니다.
12. Thread를 늘려도 처리량이 개선되지 않는 경우는?
병목 Resource를 측정하지 않고 Thread만 늘리면 Context Switch 비용만 커질 수 있습니다.
13. Action Cancel 진행성을 위해 필요한 것은?
Scheduling과 Application 안전 처리가 모두 필요합니다.
14. Hard Real-time 요구에서 기본 Executor만으로 부족할 수 있는 이유는?
WaitSet, rclc Executor, RT OS와 WCET 분석 같은 별도 설계를 검토합니다.
15. Executor를 바꾸기 전에 먼저 할 일은?
측정된 병목에 맞춰 Callback 분리·비동기화·Group·Thread 수를 결정합니다.
- 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입니다.
연습 문제
- 자신의 Node에서 Subscription·Timer·Service·Action Callback 목록과 Group 배치표를 작성하세요.
- SingleThreaded BlockingDemo의 fast Timer 간격 max·p95를 측정하세요.
- Executor만 MultiThreaded로 바꾸었을 때 결과가 같은 이유를 설명하세요.
- 두 Timer를 별도 MutuallyExclusive Group으로 분리하고 다시 측정하세요.
- Thread 1·2·4·8개 조건에서 CPU와 최악 지연을 비교하세요.
- sleep 기반 I/O 흉내와 순수 Python CPU 계산의 차이를 비교하세요.
- 같은 기본 Group의 동기 Service Deadlock을 격리 환경에서 재현하세요.
- 위 Service Client를
call_async와 Done Callback 구조로 수정하세요. - Pose·Timestamp의 Race Condition Test와 Snapshot 해법을 구현하세요.
- Lock 안에서 Publish하는 Code를 찾아 Critical Section을 줄이세요.
- Action Execute 중 Cancel이 100 ms 이내 처리되는지 시험하세요.
- Scan Message Age가 250 ms를 넘으면 정지하는 Watchdog를 구현하세요.
- Python과 C++ MultiThreaded 예제의 CPU-bound 결과를 비교하세요.
- Component Container의 Executor Type·Thread 수를 바꾸어 측정하세요.
- 실제 Robot 적용 전 Failure Injection·E-stop·Rollback Checklist를 작성하세요.
COMMUNITY
강의 댓글
질문과 학습 경험을 함께 나눠보세요.댓글을 불러오는 중입니다.