1293.57 비동기 노드의 Running 반환과 재진입

1. 비동기 노드의 Running 반환

비동기 노드(asynchronous node)는 단일 Tick 내에서 작업이 완료되지 않을 때 RUNNING 상태를 반환한다. RUNNING 반환은 작업이 진행 중이며 아직 최종 결과(SUCCESS 또는 FAILURE)가 결정되지 않았음을 부모 노드에 통보하는 메커니즘이다. 이를 통해 행동 트리는 장시간 실행되는 작업을 비차단(non-blocking) 방식으로 관리할 수 있다(Colledanchise & Ogren, 2018).

비동기 노드의 RUNNING 반환은 다음의 두 가지 시점에서 발생한다.

  1. 작업 개시 시: 노드가 IDLE 상태에서 처음 Tick될 때, 외부 작업을 개시하고 RUNNING을 반환한다.
  2. 작업 진행 중: 노드가 이미 RUNNING 상태에서 재Tick될 때, 작업 완료 여부를 확인하고 미완료 시 RUNNING을 반환한다.
class NavigateToGoal : public BT::StatefulActionNode {
public:
    BT::NodeStatus onStart() override {
        // 외부 작업 개시: ROS2 액션 목표 전송
        auto goal = NavigateToPose::Goal();
        getInput("target_pose", goal.pose);
        action_client_->async_send_goal(goal);
        return BT::NodeStatus::RUNNING;  // 작업 개시, RUNNING 반환
    }

    BT::NodeStatus onRunning() override {
        auto status = action_client_->get_status();
        if (status == GoalStatus::STATUS_SUCCEEDED) {
            return BT::NodeStatus::SUCCESS;
        }
        if (status == GoalStatus::STATUS_ABORTED) {
            return BT::NodeStatus::FAILURE;
        }
        return BT::NodeStatus::RUNNING;  // 작업 진행 중, RUNNING 유지
    }

    void onHalted() override {
        action_client_->async_cancel_goal();
    }
};

2. 재진입(Re-entry)의 정의

재진입이란, RUNNING 상태에 있는 비동기 노드가 후속 Tick에서 다시 Tick되는 것을 의미한다. 재진입 시 노드는 작업을 처음부터 다시 시작하는 것이 아니라, 진행 중인 작업의 상태를 확인하여 결과를 판정한다. StatefulActionNode에서 재진입은 onStart()가 아닌 onRunning()의 호출로 구현된다(Faconti, 2022).

Tick 1: 노드 상태 IDLE   → onStart() 호출   → RUNNING 반환 → 상태 RUNNING으로 전이
Tick 2: 노드 상태 RUNNING → onRunning() 호출 → RUNNING 반환 → 상태 RUNNING 유지
Tick 3: 노드 상태 RUNNING → onRunning() 호출 → RUNNING 반환 → 상태 RUNNING 유지
Tick 4: 노드 상태 RUNNING → onRunning() 호출 → SUCCESS 반환 → 상태 IDLE로 전이

3. 재진입의 내부 상태 판별 메커니즘

StatefulActionNode는 노드의 현재 상태에 따라 호출할 메서드를 자동으로 판별한다. 이 판별은 tick() 메서드 내부에서 수행된다.

// StatefulActionNode의 tick() 내부 로직 (개념적 구현)
BT::NodeStatus StatefulActionNode::tick() {
    if (status() == NodeStatus::IDLE) {
        NodeStatus new_status = onStart();
        return new_status;
    } else if (status() == NodeStatus::RUNNING) {
        NodeStatus new_status = onRunning();
        return new_status;
    }
    return status();
}

이 메커니즘에 의해 개발자는 작업 개시 로직(onStart())과 상태 확인 로직(onRunning())을 분리하여 구현할 수 있다. 재진입 시 작업을 중복 개시하는 오류가 구조적으로 방지된다.

4. 재진입과 부모 제어 노드의 상호작용

비동기 노드가 RUNNING을 반환하면, 부모 제어 노드의 동작은 제어 노드의 유형에 따라 결정된다.

4.1 Sequence에서의 재진입

Sequence는 자식이 RUNNING을 반환하면 자신도 RUNNING을 반환하고, 다음 Tick에서 해당 자식에 대해 재진입을 수행한다. WithMemory 모드에서는 RUNNING 자식의 인덱스를 기억하여 이전 자식을 건너뛴다.

Tick 1: Sequence → Child_0(SUCCESS) → Child_1(RUNNING) → Sequence 반환: RUNNING
Tick 2: Sequence → Child_1 재진입(RUNNING) → Sequence 반환: RUNNING
Tick 3: Sequence → Child_1 재진입(SUCCESS) → Child_2(RUNNING) → Sequence 반환: RUNNING

4.2 ReactiveSequence에서의 재진입

ReactiveSequence는 재진입 시에도 첫 번째 자식부터 다시 평가한다. RUNNING 자식에 대한 재진입은 선행 조건들이 모두 SUCCESS를 반환한 이후에 수행된다.

Tick 1: ReactiveSequence → Cond_0(SUCCESS) → Action_1(RUNNING) → 반환: RUNNING
Tick 2: ReactiveSequence → Cond_0(SUCCESS) → Action_1 재진입(RUNNING) → 반환: RUNNING
Tick 3: ReactiveSequence → Cond_0(FAILURE) → Action_1에 Halt → 반환: FAILURE

4.3 Fallback에서의 재진입

Fallback은 자식이 RUNNING을 반환하면 자신도 RUNNING을 반환하고, 다음 Tick에서 해당 자식에 대해 재진입을 수행한다.

Tick 1: Fallback → Child_0(FAILURE) → Child_1(RUNNING) → Fallback 반환: RUNNING
Tick 2: Fallback → Child_1 재진입(RUNNING) → Fallback 반환: RUNNING
Tick 3: Fallback → Child_1 재진입(SUCCESS) → Fallback 반환: SUCCESS

5. 재진입 횟수와 작업 지속 시간의 관계

비동기 노드의 재진입 횟수는 외부 작업의 지속 시간과 Tick 주기에 의해 결정된다.

N_{reentry} = \left\lceil \frac{T_{task}}{T_{tick}} \right\rceil - 1

여기서 T_{task}는 외부 작업의 소요 시간이고, T_{tick}은 Tick 주기이다. 네비게이션 작업이 10초 소요되고 Tick 주기가 100ms인 경우, 약 99회의 재진입이 발생한다. 각 재진입에서 onRunning()이 호출되어 상태를 확인한다.

6. 재진입 시 상태 확인의 효율성

재진입 시 onRunning()에서 수행되는 상태 확인은 경량 연산이어야 한다. 실제 작업은 외부 스레드나 외부 시스템에서 수행되므로, onRunning()은 해당 작업의 완료 여부만을 확인하면 된다.

T_{onRunning} \ll T_{tick} \quad \text{(이상적 조건)}

상태 확인이 과중하면 Tick 실행 시간이 증가하여 시스템 응답성이 저하된다. ROS2 액션 클라이언트의 상태 조회, 공유 변수의 플래그 확인, 퓨처(future) 객체의 완료 확인 등이 경량 상태 확인의 전형적 구현이다.

7. RUNNING 반환의 전파

비동기 노드의 RUNNING 반환은 부모 노드를 통해 루트까지 전파된다. 트리 내의 어떤 노드가 RUNNING을 반환하면, 해당 노드부터 루트까지의 경로 상의 모든 조상 노드가 RUNNING을 반환한다.

Root (RUNNING)
└── Sequence (RUNNING)
    ├── ConditionCheck (SUCCESS, 이미 완료)
    └── NavigateToGoal (RUNNING)    ← 비동기 노드

이 전파에 의해 루트 노드는 트리 전체의 실행이 아직 진행 중임을 인지하고, 다음 Tick을 예약한다.

8. 재진입 실패와 Halt의 관계

RUNNING 상태의 노드가 재진입 대신 Halt를 수신하는 경우가 존재한다. 부모 제어 노드가 다른 자식의 결과에 의해 RUNNING 자식을 중단시켜야 할 때, 재진입 대신 onHalted()가 호출된다.

Tick N:   ReactiveSequence → Cond_0(SUCCESS) → Action_1(RUNNING) → 반환: RUNNING
Tick N+1: ReactiveSequence → Cond_0(FAILURE) → Action_1에 Halt(재진입 아님) → 반환: FAILURE

이 경우 Action_1은 재진입을 기대하지만, 조건 변화에 의해 Halt가 호출되어 진행 중인 작업이 취소된다. onHalted()에서 외부 작업의 정리가 수행되어야 한다.


참고 문헌

  • 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/