1296.26 CoroActionNode의 yield 기반 Running 반환
1. yield 기반 Running 반환의 개요
CoroActionNode에서 RUNNING 상태의 반환은 setStatusRunningAndYield() 메서드의 호출에 의해 수행된다. 이 메서드는 노드의 상태를 RUNNING으로 설정하는 동시에 코루틴의 실행을 중단(yield)하여, 제어를 executeTick()의 호출자에게 반환한다. 이 메커니즘은 StatefulActionNode에서 콜백이 NodeStatus::RUNNING을 명시적으로 반환하는 방식과 근본적으로 다른 실행 모델을 형성한다.
2. setStatusRunningAndYield()의 내부 동작
setStatusRunningAndYield()의 내부 동작을 단계별로 분석한다.
void CoroActionNode::setStatusRunningAndYield()
{
// 1단계: 노드 상태를 RUNNING으로 설정
setStatus(NodeStatus::RUNNING);
// 2단계: 코루틴 실행을 중단하고 호출자에게 제어 반환
yield(); // 코루틴 라이브러리의 yield 호출
}
2.1 단계: 상태 설정
setStatus(NodeStatus::RUNNING)에 의해 노드의 상태가 RUNNING으로 설정된다. 이 상태는 executeTick()의 반환값으로 전파되어, 부모 제어 노드에 이 액션이 아직 진행 중임을 통지한다.
2.2 단계: 코루틴 중단
yield() 호출에 의해 코루틴의 실행이 현재 지점에서 중단된다. 제어는 코루틴을 재개한 executeTick()으로 반환되며, executeTick()은 RUNNING 상태를 반환한다. 코루틴의 실행 컨텍스트(프로그램 카운터, 스택 프레임, 지역 변수)는 코루틴 프레임에 보존된다.
3. executeTick()과 yield의 상호 작용
CoroActionNode의 executeTick()은 코루틴의 상태에 따라 코루틴을 생성하거나 재개한다. 이 메서드는 final로 선언되어 있어 파생 클래스에서 오버라이드할 수 없다.
NodeStatus CoroActionNode::executeTick()
{
// 코루틴이 존재하지 않으면 새로 생성
if (!coroutine_)
{
coroutine_ = createCoroutine([this]()
{
setStatus(tick()); // tick()의 반환값으로 상태 설정
});
}
// 코루틴 재개
coroutine_.resume();
// 코루틴이 완료되었으면 정리
if (coroutine_.done())
{
coroutine_.reset();
}
return status();
}
executeTick()과 tick() 내의 setStatusRunningAndYield() 사이의 상호 작용을 시간 축에서 기술한다.
executeTick() 호출
│
├─ 코루틴 미존재 → 코루틴 생성
│
├─ coroutine_.resume()
│ │
│ ├─ tick() 실행 시작 또는 재개
│ │ │
│ │ ├─ 사용자 코드 실행 ...
│ │ │
│ │ ├─ setStatusRunningAndYield() 호출
│ │ │ ├─ setStatus(RUNNING)
│ │ │ └─ yield() → 제어 반환 ←─── 중단 지점
│ │ │
│ └─ resume() 반환
│
├─ coroutine_.done() → false (아직 중단 상태)
│
└─ return status() → RUNNING
다음 Tick에서 executeTick()이 다시 호출되면 동일한 과정이 반복되되, 코루틴이 이미 존재하므로 생성 단계가 생략되고 중단 지점에서 실행이 재개된다.
4. yield와 return의 의미론적 차이
CoroActionNode에서 RUNNING의 반환은 setStatusRunningAndYield()에 의한 yield로 수행되고, SUCCESS와 FAILURE의 반환은 tick() 메서드의 return 문으로 수행된다. 이 두 메커니즘의 의미론적 차이를 분석한다.
4.1 yield: 일시 중단
BT::NodeStatus tick() override
{
doSomething();
setStatusRunningAndYield(); // 실행 중단, 다음 Tick에서 재개
doSomethingElse(); // 다음 Tick에서 이 지점부터 실행
return BT::NodeStatus::SUCCESS;
}
setStatusRunningAndYield() 이후의 코드는 삭제되지 않으며, 다음 Tick에서 정확히 이 지점부터 실행이 재개된다. 코루틴의 실행 컨텍스트가 보존되므로, doSomething()에서 설정된 지역 변수의 값이 doSomethingElse()에서 그대로 참조 가능하다.
4.2 return: 완료
BT::NodeStatus tick() override
{
doSomething();
return BT::NodeStatus::SUCCESS; // 코루틴 종료
// 이후 코드는 도달 불가
}
return 문에 의해 코루틴이 완료(done) 상태가 되고, executeTick()에서 코루틴이 정리된다. 다음 executeTick() 호출 시 새로운 코루틴이 생성되어 tick()이 처음부터 실행된다.
| 메커니즘 | 동작 | 코루틴 상태 | 다음 Tick 동작 |
|---|---|---|---|
setStatusRunningAndYield() | 중단 후 RUNNING 반환 | 중단됨 (suspended) | 중단 지점에서 재개 |
return SUCCESS | 완료 후 SUCCESS 반환 | 완료됨 (done) | 새 코루틴 생성, tick() 처음부터 |
return FAILURE | 완료 후 FAILURE 반환 | 완료됨 (done) | 새 코루틴 생성, tick() 처음부터 |
5. 다중 yield 지점의 활용
단일 tick() 메서드 내에 다수의 setStatusRunningAndYield() 호출을 배치하여, 다단계 행동을 순차적으로 기술할 수 있다.
BT::NodeStatus tick() override
{
// Phase 1: 목표 전송
auto goal = createGoal();
sendGoal(goal);
setStatusRunningAndYield(); // yield 지점 1
// Phase 2: 목표 수락 대기
while (!isGoalAccepted())
{
if (isGoalRejected())
return BT::NodeStatus::FAILURE;
setStatusRunningAndYield(); // yield 지점 2 (루프)
}
// Phase 3: 실행 완료 대기
while (!isExecutionDone())
{
auto feedback = getLatestFeedback();
setOutput("progress", feedback.percent_complete);
setStatusRunningAndYield(); // yield 지점 3 (루프)
}
// Phase 4: 결과 처리
auto result = getResult();
setOutput("result", result);
return result.success ? BT::NodeStatus::SUCCESS
: BT::NodeStatus::FAILURE;
}
이 구현에서 각 yield 지점은 Tick 경계를 형성한다. 코루틴이 어느 yield 지점에서 중단되었는지는 코루틴 프레임의 프로그램 카운터에 의해 추적되며, 개발자가 현재 단계를 별도의 변수로 관리할 필요가 없다.
5.1 Tick별 실행 추적
위 코드의 Tick별 실행 흐름을 추적한다.
| Tick | 실행 구간 | yield 지점 | 반환 상태 |
|---|---|---|---|
| 1 | Phase 1 (목표 전송) | yield 지점 1 | RUNNING |
| 2 | Phase 2 (수락 확인, 미수락) | yield 지점 2 | RUNNING |
| 3 | Phase 2 (수락 확인, 미수락) | yield 지점 2 | RUNNING |
| 4 | Phase 2 (수락 확인, 수락됨) → Phase 3 (진행 중) | yield 지점 3 | RUNNING |
| 5 | Phase 3 (진행 중) | yield 지점 3 | RUNNING |
| 6 | Phase 3 (완료) → Phase 4 (결과 처리) | — | SUCCESS |
Tick 4에서 Phase 2의 조건이 충족되어 while 루프를 탈출한 후, 동일한 Tick 내에서 Phase 3으로 진입하여 yield 지점 3에서 중단된다. 이는 코루틴의 실행이 yield 호출에 의해서만 중단되며, while 루프의 탈출 자체는 중단을 유발하지 않음을 보여준다.
6. StatefulActionNode의 RUNNING 반환과의 비교
StatefulActionNode에서 RUNNING의 반환은 콜백 메서드의 return NodeStatus::RUNNING 문에 의해 수행된다. 이 경우 콜백의 실행 컨텍스트는 파괴되며, 다음 Tick에서 onRunning() 콜백이 처음부터 실행된다.
6.1 StatefulActionNode의 RUNNING 반환
BT::NodeStatus onRunning() override
{
// 매 Tick마다 처음부터 실행
switch (phase_)
{
case Phase::WAIT_ACCEPT:
if (isGoalAccepted())
{
phase_ = Phase::WAIT_COMPLETE;
}
return BT::NodeStatus::RUNNING;
case Phase::WAIT_COMPLETE:
if (isExecutionDone())
{
return getResult().success
? BT::NodeStatus::SUCCESS
: BT::NodeStatus::FAILURE;
}
return BT::NodeStatus::RUNNING;
}
return BT::NodeStatus::FAILURE;
}
6.2 CoroActionNode의 yield 기반 RUNNING 반환
BT::NodeStatus tick() override
{
// 중단 지점에서 재개
while (!isGoalAccepted())
{
setStatusRunningAndYield();
}
while (!isExecutionDone())
{
setStatusRunningAndYield();
}
return getResult().success
? BT::NodeStatus::SUCCESS
: BT::NodeStatus::FAILURE;
}
| 비교 항목 | CoroActionNode yield | StatefulActionNode return |
|---|---|---|
| 실행 재개 위치 | yield 지점 | 콜백 시작점 |
| 지역 변수 보존 | 자동 | 불가 (멤버 변수 필요) |
| 단계 추적 | 암묵적 (프로그램 카운터) | 명시적 (phase 변수) |
| 코드 구조 | 순차적 흐름 | 상태 분기 |
| RUNNING 반환 방법 | setStatusRunningAndYield() | return RUNNING |
| 반환 후 코루틴/콜백 상태 | 중단됨 | 종료됨 |
7. yield와 Halt의 상호 작용
코루틴이 yield에 의해 중단된 상태에서 Halt가 발생하면, 코루틴이 파괴된다. 이 과정에서 코루틴 프레임에 저장된 지역 변수의 소멸자가 호출된다.
BT::NodeStatus tick() override
{
auto resource = std::make_unique<Resource>();
setStatusRunningAndYield(); // 중단 지점
// Halt가 발생하면 코루틴 파괴
// → resource의 소멸자 호출 → 자원 해제
// → 이 이후의 코드는 실행되지 않음
resource->process();
return BT::NodeStatus::SUCCESS;
}
yield 지점에서 중단된 코루틴이 Halt에 의해 파괴되는 과정을 도식화한다.
코루틴 상태: 중단됨 (yield 지점에서)
│
├─ halt() 호출
│ │
│ ├─ 코루틴 파괴 시작
│ │ ├─ 지역 변수 소멸자 호출 (역순)
│ │ │ └─ resource.~unique_ptr() → Resource 해제
│ │ └─ 코루틴 프레임 메모리 해제
│ │
│ └─ 코루틴 객체 초기화 (nullptr)
│
└─ 노드 상태: IDLE
RAII 패턴을 통한 자원 관리는 코루틴의 파괴 시에도 자원 누수를 방지한다. 그러나 원시 포인터(raw pointer)에 의한 자원 관리는 소멸자가 호출되지 않으므로, Halt 시 자원이 누수된다.
// 위험: 원시 포인터 사용
BT::NodeStatus tick() override
{
Resource* resource = new Resource();
setStatusRunningAndYield();
// Halt 시 delete가 호출되지 않음 → 메모리 누수
delete resource;
return BT::NodeStatus::SUCCESS;
}
따라서 CoroActionNode의 tick() 내부에서 관리하는 자원은 반드시 RAII 객체(std::unique_ptr, std::lock_guard, std::shared_ptr 등)를 통해 관리하여야 한다.
8. yield 빈도와 트리 반응성
setStatusRunningAndYield() 호출의 빈도는 트리의 반응성에 직접적으로 영향을 미친다. yield 호출 사이의 실행 시간이 길어지면, 해당 시간 동안 메인 스레드가 점유되어 트리의 다른 노드가 실행되지 못한다.
yield 사이의 최대 실행 시간을 T_{\text{segment}}, Tick 주기를 T_{\text{period}}, 동시에 RUNNING 상태인 코루틴 수를 n이라 하면, 다음의 조건이 충족되어야 트리의 실시간성이 보장된다.
n \cdot T_{\text{segment}} < T_{\text{period}}
이 조건에서 각 코루틴의 T_{\text{segment}}는 가능한 한 짧게 유지하여야 하며, 비차단적 확인(non-blocking check)과 경량 연산만을 yield 사이에 배치하는 것이 권장된다.
설계 지침
-
RUNNING 반환에는 setStatusRunningAndYield()만 사용:
tick()내부에서return NodeStatus::RUNNING을 사용하지 않는다. 이는 코루틴을 완료시키며, 다음 Tick에서tick()이 처음부터 실행되어 의도하지 않은 동작이 발생한다. -
yield 사이의 비차단성 보장: yield 호출 사이에 차단 호출을 배치하지 않는다. 비차단적 폴링과 경량 연산만을 수행한다.
-
RAII 기반 자원 관리: 지역 변수로 관리되는 자원은 RAII 패턴을 적용하여, Halt에 의한 코루틴 파괴 시에도 자원이 적절히 해제되도록 한다.
-
yield 빈도의 적절성: 장시간의 연산을 단일 yield 구간에 배치하지 않는다. 작업을 작은 단위로 분할하여 각 단위 사이에 yield를 삽입한다.
-
반환값의 명확성:
tick()의 최종 반환은 SUCCESS 또는 FAILURE만 사용하며, RUNNING은 오직setStatusRunningAndYield()를 통해서만 반환한다(Faconti, 2022).