타이머의 기본 개념

ROS2에서 타이머는 주기적인 작업을 처리하는 데 매우 중요한 역할을 한다. 타이머는 특정 주기로 함수(콜백 함수)를 호출하는 메커니즘을 제공하며, 실시간 시스템에서 주기적 작업을 효율적으로 관리할 수 있게 도와준다.

타이머는 노드에서 생성되며, 주기적으로 실행되어야 하는 작업을 관리하는 데 사용된다. 예를 들어, 센서 데이터의 주기적인 수집, 로봇의 주기적인 제어 명령 송신, 로그 데이터 저장 등의 작업을 타이머를 통해 처리할 수 있다.

타이머 생성 방법

ROS2에서 타이머를 생성하는 방법은 다음과 같다. rclcpp::Node 클래스에는 타이머를 쉽게 생성할 수 있는 API가 제공된다.

auto timer = this->create_wall_timer(
    std::chrono::milliseconds(1000),
    std::bind(&MyNode::timer_callback, this));

위의 코드는 1초(1000ms) 주기로 timer_callback 함수를 호출하는 타이머를 생성한다. create_wall_timer 함수는 다음 두 가지 매개변수를 받는다.

  1. 주기 (std::chrono::duration): 호출 주기. 이 경우, 1초이다.
  2. 콜백 함수: 주기적으로 호출될 함수. 이 경우, timer_callback이라는 메서드이다.

주기적 작업 관리

타이머를 통해 주기적으로 실행될 작업을 관리할 때 고려해야 할 중요한 요소는 작업의 실행 시간이 타이머 주기보다 길지 않아야 한다는 점이다. 그렇지 않으면 작업이 중첩되거나 누락될 수 있다. 이를 방지하기 위해 작업의 실행 시간을 측정하고, 필요시 적절한 동기화 메커니즘을 적용해야 한다.

타이머와 콜백 큐

ROS2에서는 타이머가 생성되면 해당 타이머는 콜백 큐에 등록된다. 타이머가 만료되면 ROS2의 이벤트 루프에서 해당 타이머의 콜백을 실행한다. 타이머 콜백이 실행될 때는 일반적으로 노드의 메시지 큐에서 다른 콜백들과 함께 처리된다.

타이머와 메시지 처리가 동일한 콜백 큐에서 처리될 경우, 타이머 콜백은 메시지 콜백과 경쟁하여 처리 순서가 결정된다. 이러한 동작은 실시간 성능에 영향을 미칠 수 있으므로 주의가 필요하다.

graph LR A(타이머 생성) --> B(콜백 큐에 등록) B --> C(타이머 만료 시 콜백 호출) C --> D(타이머 콜백 함수 실행)

ROS2 타이머와 노드 스핀

타이머는 노드의 스핀 기능과 함께 작동한다. 노드가 스핀될 때, 콜백 큐에 등록된 타이머와 메시지가 처리된다. 즉, 타이머는 노드의 스핀 루프 안에서만 동작하며, 스핀하지 않으면 타이머가 실행되지 않는다.

rclcpp::spin(node);

위의 코드는 노드를 스핀하는 코드이다. 이 코드를 실행해야 타이머가 주기적으로 호출된다.

타이머와 실시간 시스템에서의 고려 사항

실시간 시스템에서 타이머를 사용할 때 주의해야 할 중요한 점은 타이머의 주기와 실제 작업의 수행 시간이 일치하지 않을 수 있다는 점이다. 특히, 타이머 콜백이 수행하는 작업이 시간이 오래 걸리면 타이머 주기가 지연될 수 있다. 이를 해결하기 위해 ROS2는 다양한 QoS(품질 서비스) 정책을 제공한다.

실시간 시스템에서는 콜백 함수의 실행 시간을 철저히 관리해야 하며, 이를 위해 다음과 같은 전략을 사용할 수 있다.

1. 작업의 분할

주기적으로 수행되는 작업이 너무 오래 걸리는 경우, 작업을 여러 부분으로 나누어 각 부분이 타이머 콜백 내에서 독립적으로 실행되도록 한다. 이렇게 하면 한 번의 콜백 실행이 완료될 때까지 걸리는 시간을 줄일 수 있다.

2. 타이머 우선순위 조정

실시간 시스템에서는 다양한 작업들이 서로 다른 우선순위를 가질 수 있다. 타이머의 우선순위를 조정하여 중요한 작업이 제때 처리되도록 하는 것이 중요하다. 우선순위가 낮은 작업은 시스템 리소스가 여유가 있을 때 처리되도록 설정할 수 있다.

3. 타이머 간의 조정

여러 개의 타이머가 있을 경우, 타이머들 간의 주기를 조정하여 특정 시간에 여러 타이머가 동시에 실행되지 않도록 할 수 있다. 이렇게 하면 시스템의 과부하를 방지할 수 있다.

4. 타이머 동기화

분산된 여러 노드에서 타이머를 사용할 경우, 타이머의 동기화가 필요할 수 있다. 이를 위해 ROS2는 system_clocksteady_clock을 사용하여 시간 동기화를 지원한다.

타이머를 활용한 주기적 작업 예시

다음 예시는 ROS2에서 타이머를 사용하여 주기적으로 센서 데이터를 수집하는 간단한 예제이다. 이 코드는 500ms마다 센서 데이터를 읽고 이를 퍼블리시하는 작업을 수행한다.

#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>

class SensorNode : public rclcpp::Node
{
public:
    SensorNode() : Node("sensor_node")
    {
        publisher_ = this->create_publisher<std_msgs::msg::String>("sensor_data", 10);
        timer_ = this->create_wall_timer(
            std::chrono::milliseconds(500),
            std::bind(&SensorNode::timer_callback, this));
    }

private:
    void timer_callback()
    {
        auto message = std_msgs::msg::String();
        message.data = "Sensor data";
        publisher_->publish(message);
        RCLCPP_INFO(this->get_logger(), "Publishing sensor data");
    }

    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
    rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<SensorNode>());
    rclcpp::shutdown();
    return 0;
}

이 코드는 500ms마다 sensor_data 토픽에 데이터를 퍼블리시하는 노드를 정의한다. 타이머는 create_wall_timer 메서드를 사용하여 생성되며, 매 500ms마다 timer_callback 함수가 호출된다.

타이머 주기의 설정

타이머 주기는 std::chrono::duration을 사용하여 설정할 수 있다. 주기는 매우 유연하게 설정 가능하며, 나노초 단위까지 지원된다. 예를 들어, 다음과 같이 100ms 단위의 타이머를 설정할 수 있다.

auto timer = this->create_wall_timer(
    std::chrono::milliseconds(100),
    std::bind(&MyNode::timer_callback, this));

이 외에도, ROS2에서는 다양한 시간 단위의 주기를 설정할 수 있다.

// 마이크로초 단위 설정
std::chrono::microseconds(1000);

// 나노초 단위 설정
std::chrono::nanoseconds(100000);

타이머와 주기적 작업의 상호 작용

타이머는 주기적인 작업을 처리하는 데 최적화되어 있지만, 복잡한 시스템에서는 타이머 간의 상호 작용을 고려해야 한다. 예를 들어, 서로 다른 주기로 동작하는 타이머들이 있을 때, 두 타이머가 같은 시간에 실행되지 않도록 하는 것이 중요할 수 있다.

이를 위해 타이머의 초기화 시점이나 주기를 조정하여 충돌을 방지하거나, 타이머 실행 시간을 모니터링하여 실행 시간이 겹치지 않도록 관리할 수 있다.

타이머의 주기 조정 및 동기화

타이머의 주기 설정은 작업의 특성에 따라 신중하게 선택해야 한다. 작업이 얼마나 자주 실행되어야 하는지, 실행 시간이 얼마나 걸리는지, 시스템의 실시간 성능 요구 사항 등을 고려해야 한다. 주기를 짧게 설정하면 작업이 더 자주 실행되지만, 이는 CPU 부하를 증가시킬 수 있다. 반대로 주기를 길게 설정하면 작업 간의 지연이 발생할 수 있다.

동기화 전략

타이머가 여러 개 존재하는 경우, 특히 실시간 시스템에서 타이머의 동기화는 매우 중요한 요소이다. 각 타이머가 독립적으로 동작하다 보면 주기적으로 실행되는 타이머들이 동시에 트리거되거나 순차적으로 실행되어야 하는 작업이 어긋날 수 있다. 이를 해결하기 위해 다음과 같은 동기화 전략을 사용할 수 있다.

  1. 타이머 오프셋 설정: 서로 다른 타이머들 간의 충돌을 방지하기 위해 오프셋을 설정하여 서로 다른 시간에 실행되도록 조정할 수 있다. 예를 들어, 1초마다 실행되는 두 타이머 중 하나에 500ms의 오프셋을 추가하여 동시에 실행되지 않도록 할 수 있다.

  2. 우선순위 기반 동기화: 중요한 작업을 먼저 실행하도록 우선순위를 설정하고, 낮은 우선순위의 작업은 시스템 리소스가 충분할 때 실행되도록 조정할 수 있다.

  3. 타이머 조율: 서로 다른 주기의 타이머가 있을 때, 주기적인 충돌을 방지하기 위해 특정 시점에 타이머들을 조율하여 동기화할 수 있다. 이를 통해 타이머 간의 간섭을 줄일 수 있다.

타이머와 주기적 작업 관리에 대한 사례

다음은 타이머를 사용하여 주기적인 작업을 처리하는 실제 사례이다. 이 코드는 1초마다 두 가지 주기적 작업을 수행하며, 하나는 1초마다, 다른 하나는 500ms마다 실행된다. 주기가 겹치는 시점에 두 작업이 동시에 실행되지 않도록 오프셋을 적용한 예시이다.

#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>

class PeriodicNode : public rclcpp::Node
{
public:
    PeriodicNode() : Node("periodic_node")
    {
        // 1초 주기 타이머
        timer_1s_ = this->create_wall_timer(
            std::chrono::seconds(1),
            std::bind(&PeriodicNode::task_1s, this));

        // 500ms 주기 타이머, 오프셋 적용
        timer_500ms_ = this->create_wall_timer(
            std::chrono::milliseconds(500),
            std::bind(&PeriodicNode::task_500ms, this));
    }

private:
    void task_1s()
    {
        RCLCPP_INFO(this->get_logger(), "Executing 1-second task");
    }

    void task_500ms()
    {
        RCLCPP_INFO(this->get_logger(), "Executing 500ms task");
    }

    rclcpp::TimerBase::SharedPtr timer_1s_;
    rclcpp::TimerBase::SharedPtr timer_500ms_;
};

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<PeriodicNode>());
    rclcpp::shutdown();
    return 0;
}

이 코드는 두 개의 타이머를 생성하여 하나는 1초마다, 다른 하나는 500ms마다 작업을 실행하도록 한다. 주기가 다르기 때문에 두 작업이 겹치는 경우도 있지만, 오프셋을 추가하여 타이머가 동시에 실행되지 않도록 조정할 수 있다.

타이머와 주기적 작업 관리의 성능 최적화

주기적 작업을 수행할 때는 성능 최적화가 매우 중요하다. 특히, 다수의 타이머와 주기적 작업이 동시에 실행될 때는 다음과 같은 최적화 방법을 고려할 수 있다.

  1. 콜백 함수의 실행 시간 최소화: 타이머의 콜백 함수에서 복잡한 연산을 수행할 경우, 실행 시간이 길어질 수 있다. 이로 인해 주기가 지연될 수 있으므로 콜백 함수에서는 최소한의 연산만 수행하고, 나머지 작업은 별도의 쓰레드나 프로세스로 처리하는 것이 좋다.

  2. 타이머의 주기 동적 조정: 시스템의 부하에 따라 타이머의 주기를 동적으로 변경할 수 있다. 예를 들어, 시스템 부하가 높은 경우에는 타이머 주기를 늘려 시스템 리소스를 절약할 수 있다.

  3. 멀티스레드 사용: 타이머 콜백 함수가 병렬로 처리될 수 있는 경우, 멀티스레드를 활용하여 성능을 향상시킬 수 있다. ROS2에서는 MultiThreadedExecutor를 사용하여 여러 타이머를 병렬로 처리할 수 있다.

rclcpp::executors::MultiThreadedExecutor executor;
executor.add_node(node);
executor.spin();

MultiThreadedExecutor는 여러 콜백을 병렬로 처리할 수 있어, 타이머 콜백이 서로 간섭하지 않고 동시에 실행될 수 있다.