1293.63 공유 자원에 대한 Tick 단위 락 관리

1293.63 공유 자원에 대한 Tick 단위 락 관리

1. Tick 단위 락 관리의 개념

Tick 단위 락 관리란, 행동 트리의 단일 Tick 실행 전체를 하나의 임계 영역으로 취급하여 공유 자원에 대한 접근을 제어하는 동기화 전략이다. Tick 시작 시 락을 획득하고 Tick 종료 시 락을 해제함으로써, Tick 실행 중에는 외부 스레드가 공유 자원을 수정하지 못하도록 보장한다. 이를 통해 단일 Tick 내에서 모든 노드가 일관된 데이터를 읽을 수 있다(Faconti, 2022).

2. Tick 단위 락의 동작 모델

Tick 스레드:
  락 획득 ─────────────────────────── 락 해제
  │  Node_A.tick() → 공유 자원 읽기     │
  │  Node_B.tick() → 공유 자원 읽기     │
  │  Node_C.tick() → 공유 자원 쓰기     │
  └────────────────────────────────────┘

콜백 스레드:
  ──── 대기 ─────────────────────────── 락 획득 → 쓰기 → 락 해제

Tick 실행 중에 콜백 스레드는 락 획득을 대기하며, Tick 완료 후에야 공유 자원을 갱신할 수 있다.

3. 구현 방식

3.1 전역 Tick 락

전체 Tick 실행을 단일 뮤텍스로 보호하는 방식이다.

class BehaviorTreeExecutor {
public:
    void tickLoop() {
        rclcpp::Rate rate(10);
        while (rclcpp::ok()) {
            executor_.spin_some();
            
            {
                std::lock_guard<std::mutex> lock(tick_mutex_);
                tree_.tickOnce();
            }
            
            rate.sleep();
        }
    }

    std::mutex& getTickMutex() { return tick_mutex_; }

private:
    std::mutex tick_mutex_;
    BT::Tree tree_;
    rclcpp::executors::SingleThreadedExecutor executor_;
};

콜백에서 공유 자원에 접근할 때 동일한 뮤텍스를 사용한다.

void sensorCallback(const sensor_msgs::msg::LaserScan::SharedPtr msg) {
    std::lock_guard<std::mutex> lock(bt_executor_->getTickMutex());
    shared_scan_data_ = *msg;
}

3.2 읽기-쓰기 락(Reader-Writer Lock)

Tick 실행 중에는 읽기 락을, 콜백에서는 쓰기 락을 사용하여 읽기 간의 동시성을 허용하는 방식이다.

class SharedSensorData {
public:
    sensor_msgs::msg::LaserScan read() const {
        std::shared_lock<std::shared_mutex> lock(mutex_);
        return scan_;
    }

    void write(const sensor_msgs::msg::LaserScan& scan) {
        std::unique_lock<std::shared_mutex> lock(mutex_);
        scan_ = scan;
    }

private:
    mutable std::shared_mutex mutex_;
    sensor_msgs::msg::LaserScan scan_;
};

그러나 행동 트리에서 Tick은 단일 스레드에서 실행되므로, 복수의 노드가 동시에 읽기를 수행하는 경우가 발생하지 않는다. 따라서 읽기-쓰기 락의 읽기 동시성 이점은 Tick 메커니즘에서는 제한적이다.

4. 이중 버퍼링(Double Buffering) 전략

Tick 단위 락의 대안으로, 이중 버퍼링을 사용하여 락 없이 데이터 일관성을 확보하는 전략이 있다. 콜백은 백 버퍼(back buffer)에 데이터를 기록하고, Tick 시작 시 백 버퍼와 프런트 버퍼(front buffer)를 교환한다.

class DoubleBufferedData {
public:
    void updateFromCallback(const sensor_msgs::msg::LaserScan& scan) {
        std::lock_guard<std::mutex> lock(swap_mutex_);
        back_buffer_ = scan;
        new_data_available_ = true;
    }

    void swapBuffers() {
        std::lock_guard<std::mutex> lock(swap_mutex_);
        if (new_data_available_) {
            std::swap(front_buffer_, back_buffer_);
            new_data_available_ = false;
        }
    }

    const sensor_msgs::msg::LaserScan& read() const {
        // Tick 중에는 front_buffer만 읽음 (락 불필요)
        return front_buffer_;
    }

private:
    sensor_msgs::msg::LaserScan front_buffer_;
    sensor_msgs::msg::LaserScan back_buffer_;
    std::mutex swap_mutex_;
    bool new_data_available_{false};
};

Tick 루프에서의 사용:

void tickLoop() {
    while (rclcpp::ok()) {
        executor_.spin_some();
        
        // Tick 시작 전 버퍼 교환 (짧은 락)
        sensor_data_.swapBuffers();
        
        // Tick 실행 (락 없이 front_buffer 읽기)
        tree_.tickOnce();
        
        rate.sleep();
    }
}

이 방식에서 락은 버퍼 교환 시에만 짧게 보유되며, Tick 실행 중에는 락이 불필요하다. 콜백 스레드의 대기 시간이 최소화된다.

5. Tick 단위 락과 Tick 실행 시간의 관계

Tick 단위 락을 사용하면, 콜백 스레드가 Tick 실행 시간 동안 대기해야 한다. Tick 실행 시간이 T_{tick}이면, 콜백의 최대 대기 시간도 T_{tick}이다.

T_{callback\_wait} \leq T_{tick}

Tick 실행 시간이 길어지면 콜백 처리 지연이 증가하여 데이터 신선도(freshness)가 저하된다. 따라서 Tick 단위 락 전략은 Tick 실행 시간이 충분히 짧은 경우에 적합하다.

6. 노드 단위 락과의 비교

특성Tick 단위 락노드 단위 락
락 범위Tick 전체개별 노드의 데이터 접근
데이터 일관성Tick 내 전역 일관성노드별 지역 일관성
콜백 대기 시간Tick 실행 시간 전체개별 접근 시간
구현 복잡도낮음 (단일 락)높음 (노드별 락 관리)
교착 상태 위험낮음복수 락 시 존재
확장성낮음 (전체 차단)높음 (세밀한 제어)

7. 하이브리드 접근

실무에서는 Tick 단위 락과 노드 단위 락을 결합한 하이브리드 접근이 유효하다. 빈번히 갱신되는 센서 데이터는 이중 버퍼링으로 처리하고, 드물게 접근되는 설정 데이터는 Tick 단위 락으로 보호한다.

센서 데이터 (고빈도): 이중 버퍼링 → Tick 시작 시 교환
설정 데이터 (저빈도): Tick 단위 락 → Tick 중 안전한 읽기
상태 플래그 (단순): std::atomic → 락 불필요

8. 락 경합의 모니터링

락 경합이 심하면 Tick 실행 지연이나 콜백 처리 지연이 발생한다. 락 경합의 지표로는 락 획득 대기 시간, 콜백 처리 지연, Tick 실행 시간의 분산 등을 측정하여 동기화 전략의 적절성을 평가할 수 있다.

\text{contention\_ratio} = \frac{T_{lock\_wait}}{T_{tick}} \times 100\%

경합 비율이 높으면 락 범위를 축소하거나 이중 버퍼링으로 전환하는 것이 바람직하다.


참고 문헌

  • 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/
  • Williams, A. (2019). C++ Concurrency in Action (2nd ed.). Manning Publications.