核心概念
Callback Group(回調組)是一個管理一個或多個回調函數執行規則的容器。它決定了這些回調函數是如何被節點(Node)的 executor 調度的,特別是當多個回調函數同時就緒時,它們之間是并行執行還是必須串行執行。
理解回調組的關鍵在于理解它與 Executor 的交互。Executor 是 ROS 2 中負責檢查訂閱、定時器、服務、動作服務器等是否就緒,并調用其對應回調函數的組件。
為什么需要 Callback Group?
例如一個節點,它有以下組件:
- 一個激光雷達(Lidar)訂閱者:回調函數
lidar_callback
,處理高頻、數據量大的點云數據。 - 一個鍵盤輸入訂閱者:回調函數
keyboard_callback
,處理用戶偶爾的按鍵輸入。 - 一個服務服務器:回調函數
save_data_service_callback
,處理保存數據的請求。
如果沒有回調組,所有回調函數默認都在同一個組里。當 Executor 發現多個回調函數同時就緒時(例如,正在處理激光數據時用戶按下了鍵),它會將它們全部放入一個隊列,然后逐個執行。
這可能會導致一個問題:處理龐大的激光雷達數據可能會阻塞 keyboard_callback
或 save_data_service_callback
很長時間,導致用戶輸入或服務請求響應非常遲鈍,體驗很差。
回調組就是為了解決這種回調函數之間的相互干擾問題而設計的。
回調組的類型
ROS 2 主要提供了兩種類型的回調組:
1. Mutually Exclusive (互斥型) - 默認類型
- 行為:屬于同一個互斥組的所有回調函數不能同時執行。Executor 會像處理一個隊列一樣,一次只執行其中一個。
- 類比:單線程。任務一個接一個地做。
- 適用場景:這是最常用也是默認的類型。適用于回調函數之間沒有嚴格的實時性要求,或者它們會訪問共享資源需要互斥鎖保護的情況。
2. Reentrant (可重入型)
- 行為:屬于同一個可重入組的所有回調函數可以同時被執行。Executor 會盡可能同時調用它們(如果系統有多個CPU核心,則真正并行)。
- 警告:使用此類型需要非常小心。你必須確保回調函數是線程安全的。如果它們會訪問共享的變量或資源,你必須自己使用鎖(如
std::mutex
)來保護,否則會導致數據競爭和未定義行為。 - 類比:多線程。多個任務可以同時進行。
- 適用場景:適用于那些相互獨立、沒有共享數據、且對實時性要求很高的回調函數。例如,一個控制電機的高頻定時器回調和一個發布狀態的低頻定時器回調。
使用回調組
你需要在創建訂閱者、定時器、服務等之前先創建回調組,然后在創建這些對象時將回調組作為參數傳入。
以下是 C++ 和 Python 的示例代碼:
C++ 示例
#include “rclcpp/rclcpp.hpp”class MyNode : public rclcpp::Node {
public:MyNode() : Node(”my_node”) {// 1. 創建回調組// 互斥型 (默認就是這個,顯式寫出更清晰)mutually_exclusive_group_ = this- >create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);// 可重入型reentrant_group_ = this- >create_callback_group(rclcpp::CallbackGroupType::Reentrant);// 2. 設置選項,在創建對象時指定所屬的回調組rclcpp::SubscriptionOptions options;options.callback_group = mutually_exclusive_group_;// 創建訂閱者,并指定它屬于 mutually_exclusive_group_lidar_sub_ = this- >create_subscription< sensor_msgs::msg::LaserScan >(”/scan”, 10,std::bind(&MyNode::lidar_callback, this, std::placeholders::_1),options); // 傳入 options// 對于不需要特殊選項的對象,可以直接傳遞 callback_group 參數// 這個定時器屬于 reentrant_group_,可以和上面的訂閱者并行執行timer_ = this- >create_wall_timer(std::chrono::seconds(1),std::bind(&MyNode::timer_callback, this),reentrant_group_); // 直接傳入回調組}private:void lidar_callback(const sensor_msgs::msg::LaserScan::SharedPtr msg) {// 處理激光數據,可能很耗時}void timer_callback() {// 定時執行的任務}rclcpp::CallbackGroup::SharedPtr mutually_exclusive_group_;rclcpp::CallbackGroup::SharedPtr reentrant_group_;rclcpp::Subscription< sensor_msgs::msg::LaserScan >::SharedPtr lidar_sub_;rclcpp::TimerBase::SharedPtr timer_;
};int main(int argc, char * argv[]) {rclcpp::init(argc, argv);auto node = std::make_shared< MyNode >();rclcpp::spin(node);rclcpp::shutdown();return 0;
}
回調組與執行器(Executor)的配合
- SingleThreadedExecutor:即使是可重入組,回調函數也無法真正并行,因為只有一個線程。但 Executor 會通過技巧(如在等待服務響應時切換到其他可執行回調)來模擬并發,提高響應效率。
- MultiThreadedExecutor:這是發揮可重入組威力的地方。該執行器內部有一個線程池。當一個可重入組有多個回調就緒時,執行器可以從線程池中取出多個線程來真正并行地執行它們。
最佳實踐:如果你使用了可重入回調組,通常應該配合 MultiThreadedExecutor
來使用,以實現真正的并行處理。
總結
特性 | Mutually Exclusive (互斥) | Reentrant (可重入) |
---|---|---|
執行方式 | 串行 | 并行 |
線程安全 | 不需要額外考慮(默認安全) | 必須自行保證線程安全 |
性能 | 可能因長回調導致阻塞 | 響應性更好,資源利用率更高 |
適用場景 | 默認選擇,共享資源需保護 | 實時性要求高,回調間完全獨立 |
核心:Callback Group 是一種強大的工具,讓你能夠精細地控制節點內部回調函數的執行流程,從而優化節點的響應性和性能,避免不必要的阻塞。在設計復雜的節點時,合理地使用回調組是非常重要的一環。