Atlas Robotics
ROS2 nâng caoNâng cao

Callback Group — kiểm soát callback nào chạy song song được

Atlasbot · 17/8/2026 · 11 phút đọc

Callback Group — kiểm soát callback nào chạy song song được
Hai loại Callback Group và cách chúng ảnh hưởng tới việc chạy song song

Mặc định, mọi callback (subscriber, timer, service) trong một node ROS2 thuộc cùng một nhóm gọi là MutuallyExclusiveCallbackGroup — nghĩa là chúng không bao giờ chạy đồng thời, kể cả khi node dùng MultiThreadedExecutor (xem bài Multi-threading) với nhiều luồng sẵn sàng. Một callback service xử lý chậm (đọc file lớn, gọi API mạng) sẽ chặn đứng callback đọc cảm biến khác của cùng node cho tới khi xong.

Khái niệm chính

  • MutuallyExclusiveCallbackGroup (mặc định) — các callback trong cùng group đảm bảo không bao giờ chạy đồng thời, kể cả có nhiều thread rảnh. An toàn tuyệt đối với dữ liệu chia sẻ (không cần lock), nhưng dễ bị một callback chậm chặn callback khác
  • ReentrantCallbackGroup — các callback trong cùng group được phép chạy song song với nhau, miễn là executor có đủ thread. Cần tự đảm bảo an toàn dữ liệu chia sẻ (mutex) vì giờ có thể có race condition
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup, ReentrantCallbackGroup

class MyNode(Node):
    def __init__(self):
        super().__init__("my_node")
        fast_group = ReentrantCallbackGroup()
        slow_group = MutuallyExclusiveCallbackGroup()

        self.create_subscription(LaserScan, "/scan", self.scan_cb, 10,
                                  callback_group=fast_group)
        self.create_service(Trigger, "/save_map", self.save_map_cb,
                             callback_group=slow_group)

Cấu hình trên tách hẳn hai luồng xử lý: scan_cb (đọc cảm biến, cần phản hồi nhanh) nằm trong ReentrantCallbackGroup riêng, không bao giờ bị save_map_cb (chậm, ít gọi) chặn lại — vì chúng thuộc hai group khác nhau.

Nguyên lý hoạt động

Tóm lại: Callback trong cùng một group không bao giờ chạy song song nếu là Mutually Exclusive, hoặc có thể chạy song song nếu là Reentrant. Callback ở hai group khác nhau luôn có thể chạy song song với nhau (nếu executor đủ thread), bất kể loại group nào.

Sai lầm thường gặp: đổi Executor mà quên Callback Group — nhiều người gặp callback bị "đứng hình" nghĩ do executor đơn luồng, chuyển sang MultiThreadedExecutor nhưng vẫn thấy hiện tượng cũ — lý do: mặc định mọi callback vẫn nằm chung một MutuallyExclusiveCallbackGroup, đổi executor không tự động cho phép chúng chạy song song. MultiThreadedExecutor chỉ là điều kiện cần (có đủ thread để chạy song song), Callback Group phù hợp mới là điều kiện đủ.

Kiến thức kỹ thuật

Thông số

Bảng chân trị đầy đủ về khả năng chạy song song, giả định đang dùng MultiThreadedExecutor với đủ thread:

Tình huốngChạy song song được?
2 callback cùng 1 MutuallyExclusiveCallbackGroupKhông
2 callback cùng 1 ReentrantCallbackGroupCó
2 callback ở 2 group khác nhau (bất kỳ loại nào)Có
Dùng SingleThreadedExecutor (bất kể loại group)Không bao giờ, vì chỉ có 1 thread xử lý

Ứng dụng thực tế

Callback Group chính là lời giải cho tình huống deadlock đã cảnh báo ở bài Service Communication — khi 1 node vừa là subscriber vừa cần gọi 1 service (kể cả service do chính node đó hoặc node liên quan cung cấp) ngay bên trong callback: đặt subscriber callback và service client vào 2 ReentrantCallbackGroup khác nhau (kết hợp MultiThreadedExecutor) cho phép chúng chạy chồng lấn nhau trên các luồng khác nhau, giải quyết tận gốc tình huống "node tự chờ chính mình" — giải pháp triệt để hơn nhiều so với chỉ né bằng cách luôn gọi call_async như đã khuyến nghị tạm thời ở bài đó.

Ví dụ thực tế

# TRƯỚC — deadlock tiềm ẩn: cùng group mặc định (MutuallyExclusive)
class RiskyNode(Node):
    def __init__(self):
        super().__init__("risky_node")
        self.client = self.create_client(Trigger, "/some_service")
        self.create_subscription(String, "/trigger_topic", self.callback, 10)

    def callback(self, msg):
        future = self.client.call_async(Trigger.Request())
        rclpy.spin_until_future_complete(self, future)  # có thể treo!

# SAU — tách 2 callback group riêng, dùng MultiThreadedExecutor
class SafeNode(Node):
    def __init__(self):
        super().__init__("safe_node")
        group_a = ReentrantCallbackGroup()
        group_b = ReentrantCallbackGroup()
        self.client = self.create_client(Trigger, "/some_service", callback_group=group_a)
        self.create_subscription(String, "/trigger_topic", self.callback, 10,
                                  callback_group=group_b)

Với SafeNode, khi callback (thuộc group_b) gọi service (xử lý bởi group_a), 2 group khác nhau cho phép chúng chạy trên 2 luồng riêng biệt của MultiThreadedExecutor — không còn tình huống "chờ chính mình" như RiskyNode.

Lỗi thường gặp

  • Đổi executor mà quên tách callback group — như đã nói ở phần Nguyên lý hoạt động, đây là nguyên nhân phổ biến nhất khiến hiện tượng "đứng hình" không biến mất dù đã chuyển sang MultiThreadedExecutor.
  • Dùng ReentrantCallbackGroup nhưng không bảo vệ dữ liệu chia sẻ bằng mutex — cho phép nhiều instance callback chạy song song đồng nghĩa race condition có thể xảy ra trên biến/cấu trúc dữ liệu dùng chung — cần tự thêm khoá (mutex/lock) đúng chỗ, ROS2 không tự làm việc này thay bạn.
  • Lạm dụng ReentrantCallbackGroup cho mọi callback — mất đi lợi ích "an toàn tuyệt đối, không cần lock" của MutuallyExclusive một cách không cần thiết — chỉ nên dùng Reentrant cho những callback thực sự cần chạy song song, không phải mặc định cho tất cả.
  • Tưởng nhầm không truyền callback_group nghĩa là mỗi callback tự động có group riêng — mặc định, callback không chỉ định callback_group sẽ dùng default callback group của node (thường là 1 MutuallyExclusiveCallbackGroup dùng chung) — không tự động tách biệt như có thể lầm tưởng.

Kết luận

Callback Group là công cụ chính xác để kiểm soát callback nào được phép chạy song song, giải quyết triệt để vấn đề deadlock khi gọi service lồng nhau — nhưng chỉ phát huy tác dụng khi kết hợp đúng với MultiThreadedExecutor. Quy tắc cốt lõi: cùng group Mutually Exclusive không bao giờ song song, khác group luôn có thể song song. Bài tiếp theo trong chuyên mục nói về Multi-threading — cách cấu hình executor đa luồng mà Callback Group vừa học phụ thuộc vào.

Callback Group — kiểm soát callback nào chạy song song được — Atlasbot