21.6.4.1.2. 장시간 연산이 필요한 경우 유한 상태 기계(FSM)를 쪼개어 여러 프레임에 걸쳐 연산하는 타임 슬라이싱(Time-slicing) 기법

21.6.4.1.2. 장시간 연산이 필요한 경우 유한 상태 기계(FSM)를 쪼개어 여러 프레임에 걸쳐 연산하는 타임 슬라이싱(Time-slicing) 기법

앞 장에서 디스크 I/O 같은 하드웨어 블로킹(Blocking)의 무서움을 보았다면, 이번에는 소프트웨어적 블로킹에 대해 이야기해 보자.

드론이 자율 비행을 하기 위해 1000개의 웨이포인트(Waypoint) 배열을 훑으며 가장 안전한 귀환 경로(Return-to-Launch)를 탐색해야 한다고 가정해 보자. 이 계산에 CPU가 꼬박 50ms를 써야 한다면?
이 연산을 한 번의 Run() 호출 안에서 for 문으로 처음부터 끝까지 돌려버리면, 내 모듈은 50ms 동안 버스 운전석을 점거하게 되고 그동안 1000Hz 자이로 센서는 50번이나 굶어 죽는다.

이러한 헤비급(Heavy) 연산을 처리하기 위해, 노련한 PX4 개발자들은 연산의 호흡을 찰나의 조각으로 끊어 치는 타임 슬라이싱(Time-slicing) 기법유한 상태 기계(FSM, Finite State Machine) 패턴을 결합하여 사용한다.

1. 타임 슬라이싱: 한 입 크기로 베어 물기

타임 슬라이싱의 철학은 단순하다. “1000개를 검사해야 한다면, 한 번 큐에 올라탔을 때(1프레임) 욕심부리지 말고 딱 100개만 검사한 뒤 쿨하게 양보(return)하고, 다음번 큐에 탔을 때 101번째부터 마저 검사하자.” 이다.

이를 구현하기 위해서는 객체의 **클래스 멤버 변수(상태를 기억하는 변수)**를 활용한 유한 상태 기계(FSM) 설계가 필수적이다.

// 헤더 파일 (.h)
class PathFinder : public px4::ScheduledWorkItem {
private:
    enum class State {
        IDLE,
        SEARCHING,
        FINISHED
    };
    State _state{State::IDLE};
    int _current_search_index{0}; // 어디까지 찾았는지 기억할 책갈피
    Result _search_result;

    void Run() override;
};

위와 같이 상태(_state)와 책갈피(_current_search_index)를 클래스 멤버 변수로 빼두면, Run() 함수가 종료(return)되어 메모리 스택이 싹 날아가더라도 내 수색 작업의 진척도는 객체의 힙(Heap) 메모리에 안전하게 유지된다.

2. 쪼개진 연산 루프 (The Sliced Loop)

이제 Run() 메서드 내부에서 이 상태 변수들을 이용해 어떻게 50ms짜리 뚱뚱한 연산을 5ms짜리 10조각으로 가볍게 썰어내는지(Slicing) 확인해 보자.

// 소스 파일 (.cpp)
void PathFinder::Run() {
    // 1. 상태 기계(FSM) 스위칭 루프
    switch (_state) {
        
        case State::IDLE:
            if (수색_명령_수신()) {
                _state = State::SEARCHING;
                _current_search_index = 0; // 책갈피 초기화
                ScheduleNow(); // 당장 다음 버스에 또 태워달라고 요청!
            }
            break; // 즉시 리턴 (버스 양보)

        case State::SEARCHING:
            // 2. 타임 슬라이싱: 욕심내지 말고 딱 100개(한 토막)만 처리하자!
            int end_index = _current_search_index + 100;
            if (end_index > 1000) end_index = 1000;

            for (int i = _current_search_index; i < end_index; ++i) {
                if (check_waypoint(i)) {
                    _search_result = i;
                    _state = State::FINISHED;
                    return; // 찾았으면 즉시 작업 끝내고 버스에서 내림
                }
            }

            // 100개를 뒤졌는데도 못 찾았다면?
            _current_search_index = end_index; // 책갈피를 업데이트해 두고
            
            if (_current_search_index >= 1000) {
                _state = State::FINISHED; // 다 뒤졌는데 없네.
            } else {
                // 중요: 아직 못 찾았으니, "다음 큐 사이클에 마저 찾을게!" 라고 예약
                ScheduleNow(); 
            }
            break; // 1 프레임 연산 종료, 다른 스레드에게 자원 양보!

        case State::FINISHED:
            결과_전송(_search_result);
            _state = State::IDLE;
            break;
    }
}

3. 비동기 시스템의 예술

위 코드를 실행하면, 이 모듈은 한 사이클에 고작 몇 마이크로초(us) 동안 100개의 배열 방만 훑어보고 즉시 큐를 빠져나간다. 자이로 센서 스레드가 급하게 지나가고 나면, 빈 버스에 다시 올라타 101번째부터 200번째까지 검사하고 또 빠져나간다.

비록 수색 작업의 총 완료 시간은 처음 설계했던 50ms보다 누적되어 60ms, 80ms로 조금 더 밀릴 수 있다. 하지만 픽스호크 시스템 전체의 호흡은 단 한 번도 막히지 않았으며, 드론은 완벽한 자세를 유지하며 무사히 귀환 경로를 찾아낼 수 있게 된다.


[ 21.6.장 요약 ]
우리는 구시대적인 개별 백그라운드 태스크(Task)를 버리고, px4::WorkItem이라는 스마트한 객체 단위로 모듈을 포장하여 _wq_rate_ctrl 등 여러 효율적인 스레드 풀 버스 노선에 분산 배치하는 법을 배웠다. 또한 hrt_absolute_time() 기반 정주기 스케줄링의 동적 보정 능력과, 시스템 마비를 막기 위한 블로킹 회피 기법까지 섭렵하여 완전한 ‘커널 스케줄링 친화적’ 모듈을 창조해 냈다.

이제 21 장의 마지막 산맥이자, 모듈 간의 정보를 빛의 속도로 실어 나르는 데이터 통신의 심장 21.7장, uORB 비동기 메시징 시스템의 깊은 곳으로 뛰어들어 보자.