1293.17 Tick과 ROS2 실행기의 통합
1. ROS2 실행기의 역할
ROS2 실행기(Executor)는 ROS2 노드에 등록된 콜백(callback)들의 실행을 관리하는 핵심 컴포넌트이다. 구독(subscription) 콜백, 타이머(timer) 콜백, 서비스(service) 콜백, 액션(action) 콜백 등 다양한 유형의 콜백이 실행기에 의해 스케줄링되고 실행된다. 행동 트리(Behavior Tree)의 Tick을 ROS2 환경에서 운용하기 위해서는 Tick 메커니즘을 ROS2 실행기의 콜백 스케줄링 체계에 통합해야 한다(Macenski et al., 2022).
행동 트리의 Tick과 ROS2 실행기의 통합은 두 가지 독립적인 실행 모델의 조율을 의미한다. 행동 트리는 주기적 Tick에 의해 구동되는 폴링(polling) 기반 실행 모델을 따르며, ROS2는 이벤트 기반 콜백 실행 모델을 따른다. 이 두 모델의 효과적인 통합이 행동 트리 기반 로봇 소프트웨어의 정상적인 동작을 보장한다.
2. ROS2 실행기의 유형과 특성
ROS2는 복수의 실행기 유형을 제공하며, 각 유형은 행동 트리 Tick과의 통합에서 상이한 특성을 보인다.
2.1 SingleThreadedExecutor
SingleThreadedExecutor는 모든 콜백을 단일 스레드에서 순차적으로 실행한다. 행동 트리의 Tick이 타이머 콜백으로 구현된 경우, Tick 콜백과 다른 콜백(토픽 구독, 서비스 응답 등)이 동일한 스레드에서 번갈아 실행된다. 이는 동기화 문제를 원천적으로 방지하지만, Tick 실행 중에 다른 콜백이 처리되지 못하므로 Tick 실행 시간이 길어지면 메시지 처리 지연이 발생할 수 있다.
auto executor = std::make_shared<rclcpp::executors::SingleThreadedExecutor>();
executor->add_node(bt_node);
executor->spin();
2.2 MultiThreadedExecutor
MultiThreadedExecutor는 복수의 스레드를 사용하여 콜백을 병렬로 실행한다. Tick 콜백과 토픽 구독 콜백이 동시에 실행될 수 있으므로, 행동 트리 노드가 접근하는 공유 데이터에 대한 동기화가 필요하다. 특히 블랙보드의 읽기/쓰기 연산과 ROS2 콜백에서의 데이터 갱신이 동시에 발생할 수 있으므로, 뮤텍스(mutex) 등의 동기화 메커니즘을 적용해야 한다.
auto executor = std::make_shared<rclcpp::executors::MultiThreadedExecutor>();
executor->add_node(bt_node);
executor->spin();
2.3 StaticSingleThreadedExecutor
StaticSingleThreadedExecutor는 SingleThreadedExecutor의 최적화된 변형으로, 노드 추가 시점에 콜백 목록을 정적으로 구성하여 실행 시 오버헤드를 감소시킨다. 행동 트리의 노드 구성이 런타임에 변경되지 않는 경우에 적합하며, 고주파 Tick에서의 스케줄링 오버헤드를 최소화할 수 있다.
3. 통합 패턴
3.1 패턴 1: 타이머 콜백 기반 통합
가장 일반적인 통합 패턴은 ROS2 타이머를 사용하여 Tick을 주기적으로 발생시키는 것이다. 타이머 콜백 내에서 tree.tickOnce()를 호출하면, Tick이 ROS2 실행기의 스케줄링에 완전히 통합된다.
class BtExecutorNode : public rclcpp::Node {
public:
BtExecutorNode() : Node("bt_executor") {
// 행동 트리 초기화
tree_ = factory_.createTreeFromFile("tree.xml");
// 타이머 기반 Tick 설정
tick_timer_ = this->create_wall_timer(
std::chrono::milliseconds(100),
std::bind(&BtExecutorNode::tick_callback, this)
);
}
private:
void tick_callback() {
auto status = tree_.tickOnce();
if (status == BT::NodeStatus::SUCCESS ||
status == BT::NodeStatus::FAILURE) {
tick_timer_->cancel();
}
}
BT::BehaviorTreeFactory factory_;
BT::Tree tree_;
rclcpp::TimerBase::SharedPtr tick_timer_;
};
이 패턴에서 Tick은 ROS2 실행기가 관리하는 콜백 중 하나로 취급되며, 실행기의 스케줄링 정책에 따라 다른 콜백과 공정하게 CPU 시간을 분배받는다.
3.2 패턴 2: 전용 스레드 기반 통합
Tick 실행을 ROS2 실행기의 스레드와 별도의 전용 스레드에서 수행하는 패턴이다. 이 경우 Tick은 ROS2의 콜백 스케줄링에 의존하지 않으므로 보다 정밀한 타이밍 제어가 가능하지만, ROS2 콜백과의 데이터 공유에 주의해야 한다.
class BtExecutorNode : public rclcpp::Node {
public:
BtExecutorNode() : Node("bt_executor") {
tree_ = factory_.createTreeFromFile("tree.xml");
bt_thread_ = std::thread([this]() {
auto period = std::chrono::milliseconds(100);
BT::NodeStatus status = BT::NodeStatus::RUNNING;
while (status == BT::NodeStatus::RUNNING &&
rclcpp::ok()) {
status = tree_.tickOnce();
tree_.sleep(period);
}
});
}
~BtExecutorNode() {
if (bt_thread_.joinable()) bt_thread_.join();
}
private:
BT::BehaviorTreeFactory factory_;
BT::Tree tree_;
std::thread bt_thread_;
};
이 패턴에서 ROS2 실행기는 rclcpp::spin()으로 토픽 구독과 서비스 콜백을 처리하고, 행동 트리는 별도 스레드에서 독립적으로 Tick한다. 두 스레드 간의 데이터 교환은 스레드 안전한 방식으로 이루어져야 한다.
3.3 패턴 3: spin_some 기반 수동 통합
행동 트리의 Tick 루프 내에서 rclcpp::spin_some()을 명시적으로 호출하여 ROS2 콜백을 처리하는 패턴이다. 이 방법은 Tick과 콜백 처리의 순서를 개발자가 완전히 제어할 수 있는 장점이 있다.
auto node = std::make_shared<rclcpp::Node>("bt_node");
auto period = std::chrono::milliseconds(100);
BT::NodeStatus status = BT::NodeStatus::RUNNING;
while (status == BT::NodeStatus::RUNNING && rclcpp::ok()) {
// 대기 중인 ROS2 콜백 처리
rclcpp::spin_some(node);
// 행동 트리 Tick 실행
status = tree.tickOnce();
// 다음 Tick까지 대기
tree.sleep(period);
}
이 패턴은 단일 스레드 내에서 ROS2 콜백과 행동 트리 Tick이 교대로 실행되므로 동기화가 필요 없으며, Tick 직전에 최신 센서 데이터가 콜백을 통해 갱신되는 것을 보장할 수 있다.
4. 통합 패턴의 비교
| 특성 | 타이머 콜백 | 전용 스레드 | spin_some 수동 |
|---|---|---|---|
| 타이밍 정밀도 | 실행기 의존 | 높음 | 중간 |
| 동기화 필요성 | SingleThreaded면 불필요 | 필수 | 불필요 |
| 구현 복잡도 | 낮음 | 중간 | 낮음 |
| 콜백 처리 순서 제어 | 실행기 위임 | 불가 | 완전 제어 |
| ROS2 생태계 호환성 | 높음 | 중간 | 중간 |
5. 실행기 선택 시 고려 사항
행동 트리와 ROS2 실행기를 통합할 때에는 다음 사항을 고려해야 한다.
5.1 콜백 그룹의 활용
ROS2의 콜백 그룹(Callback Group)을 활용하여 Tick 콜백과 다른 콜백의 실행 관계를 제어할 수 있다. MutuallyExclusiveCallbackGroup에 Tick 타이머와 관련 구독 콜백을 함께 등록하면, 이들이 동시에 실행되지 않도록 보장할 수 있다.
auto cb_group = this->create_callback_group(
rclcpp::CallbackGroupType::MutuallyExclusive);
tick_timer_ = this->create_wall_timer(
period, tick_callback, cb_group);
subscription_ = this->create_subscription<msg_type>(
topic, qos, sub_callback,
rclcpp::SubscriptionOptions().callback_group = cb_group);
5.2 Tick 실행과 메시지 수신의 순서
행동 트리의 조건 노드가 ROS2 토픽을 통해 수신된 데이터를 평가하는 경우, Tick 실행 전에 최신 메시지가 수신되어 있어야 정확한 판단이 가능하다. spin_some 패턴에서는 Tick 전에 spin_some()을 호출하여 이를 보장할 수 있으나, 타이머 콜백 패턴에서는 실행기의 스케줄링에 의존하므로 메시지 수신 시점이 보장되지 않는다.
5.3 실행기의 실시간 특성
표준 ROS2 실행기는 실시간 보장을 제공하지 않는다. 실시간 시스템에서의 행동 트리 운용이 필요한 경우, 실시간 실행기(예: ros2_tracing 프로젝트의 실시간 실행기)를 사용하거나, 행동 트리의 Tick을 실시간 스케줄러가 관리하는 전용 스레드에서 실행하는 것을 고려해야 한다.
참고 문헌
- Colledanchise, M., & Ogren, P. (2018). Behavior Trees in Robotics and AI: An Introduction. CRC Press.
- Faconti, D. (2022). BehaviorTree.CPP documentation and API reference. https://www.behaviortree.dev/
- Macenski, S., Foote, T., Gerkey, B., Lalancette, C., & Woodall, W. (2022). Robot Operating System 2: Design, architecture, and uses in the wild. Science Robotics, 7(66), eabm6074.