Atlas Robotics
ROS2 CommunicationTrung bình

Service: đồng bộ hay bất đồng bộ? Cơ chế bên trong

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

Service: đồng bộ hay bất đồng bộ? Cơ chế bên trong
Gọi service đồng bộ bên trong callback có thể tự deadlock

Bài "Service trong ROS2 là gì?" đã giới thiệu mô hình request-response cơ bản. Nhưng có một chi tiết quan trọng thường bị bỏ qua: gọi service theo kiểu đồng bộ (blocking) ngay bên trong một callback khác là một trong những lỗi phổ biến nhất khiến node ROS2 tự "treo" mà không rõ nguyên nhân.

Khái niệm chính

Client gọi service có hai kiểu:

  • Đồng bộ (blocking) — gọi xong, code dừng lại chờ tới khi có response mới chạy tiếp
  • Bất đồng bộ (async) — gọi xong, nhận về một Future (đối tượng đại diện cho "kết quả sẽ có trong tương lai"), code tiếp tục chạy ngay, xử lý response khi nó thực sự tới thông qua callback riêng

Vì sao gọi đồng bộ bên trong callback dễ gây treo node

Executor mặc định của ROS2 thường xử lý các callback (timer, subscriber, service...) của một node tuần tự trên cùng một luồng. Nếu bên trong một callback đang chạy, code gọi service theo kiểu blocking và ngồi chờ response — nhưng response đó lại cần được chính executor của node này xử lý (ví dụ gọi chính service do node đó cung cấp, hoặc phụ thuộc một callback khác chưa tới lượt chạy) — kết quả là node tự chờ chính mình, không bao giờ thoát ra được. Đây là lý do ROS2 khuyến nghị mạnh mẽ dùng gọi bất đồng bộ (call_async) thay vì gọi đồng bộ ở hầu hết trường hợp, khác với thói quen phổ biến hơn ở ROS1.

Tóm lại: Gọi service đồng bộ chỉ an toàn khi chắc chắn đang ở ngoài executor (ví dụ trong hàm main() của một script độc lập) — bên trong bất kỳ callback nào của node, luôn ưu tiên call_async kèm xử lý kết quả qua callback riêng.

Nguyên lý hoạt động

File .srv định nghĩa cả request lẫn response, phân tách bằng dòng ---:

# AddTwoInts.srv
int64 a
int64 b
---
int64 sum

Gọi service bất đồng bộ đúng cách — không block, xử lý kết quả qua callback riêng khi Future hoàn tất:

future = self.client.call_async(request)
future.add_done_callback(self.handle_response)  # được gọi khi có response, không chờ tại đây

def handle_response(self, future):
    response = future.result()
    self.get_logger().info(f'Kết quả: {response.sum}')
call_async(request)
       ↓
  trả về ngay 1 Future — code tiếp tục chạy, KHÔNG chờ
       ↓  (khi response thực sự tới)
  executor tự gọi handle_response(future)

Kiến thức kỹ thuật

Thông số

So sánh trực tiếp 2 cách gọi service — chọn sai kiểu cho đúng ngữ cảnh là nguồn gốc của phần lớn lỗi treo node liên quan tới service:

Tiêu chíĐồng bộ (blocking)Bất đồng bộ (call_async)
Code có dừng lại chờ khôngCóKhông, trả Future ngay
An toàn khi gọi trong callbackRủi ro deadlock caoAn toàn (khuyến nghị)
Phù hợp dùng ởScript độc lập, main()Bên trong node/callback
Độ phức tạp codeĐơn giản hơnCần thêm callback xử lý kết quả

Register / Bit / Byte

Ở tầng dưới, mỗi request service ROS2 mang theo một GUID định danh client + số thứ tự (sequence number) đính kèm trong phần header của message gửi đi — khi server xử lý xong và publish response, nó đính kèm lại đúng GUID + sequence number đó. Client dùng đúng cặp thông tin này để khớp chính xác response nào ứng với request nào của chính nó — cơ chế bắt buộc phải có vì nhiều client khác nhau có thể cùng gọi 1 server, và bản thân DDS topic phía dưới không tự đảm bảo thứ tự "request nào response đó" nếu không có định danh đi kèm.

Ứng dụng thực tế

Giải pháp triệt để hơn cho vấn đề deadlock đã nói ở trên (thay vì chỉ tránh gọi đồng bộ) là dùng callback group (chủ đề riêng ở chuyên mục ROS2 nâng cao) — đặt service client vào một ReentrantCallbackGroup cho phép nhiều callback của cùng node 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" thay vì chỉ né bằng cách luôn dùng async. Trong hệ thống điều khiển robot phức tạp — nơi 1 node đôi khi vừa là service server vừa là client của service khác — hiểu rõ 2 công cụ này (async + callback group) là bắt buộc để tránh những lỗi treo node rất khó tái hiện và debug.

Ví dụ thực tế

# AN TOÀN — gọi đồng bộ ngoài executor, trong script độc lập
def main():
    rclpy.init()
    node = rclpy.create_node('simple_client')
    client = node.create_client(AddTwoInts, 'add_two_ints')
    future = client.call_async(request)
    rclpy.spin_until_future_complete(node, future)   # chờ ở NGOÀI callback, an toàn
    print(future.result().sum)
# RỦI RO — gọi đồng bộ BÊN TRONG callback của chính node
def some_subscriber_callback(self, msg):
    future = self.client.call_async(request)
    rclpy.spin_until_future_complete(self, future)   # node tự chờ chính executor của nó!
    # → có thể treo vô thời hạn nếu response cần chính executor này xử lý

Hai đoạn code trông gần giống hệt nhau, nhưng đoạn thứ hai tiềm ẩn rủi ro treo node vì spin_until_future_complete được gọi từ bên trong một callback đang chiếm executor của chính node đó — khác biệt duy nhất là "đang đứng ở đâu khi gọi" nhưng hậu quả hoàn toàn khác nhau.

Lỗi thường gặp

  • Gọi spin_until_future_complete hoặc chờ đồng bộ bên trong callback của chính node — như đã phân tích ở trên, đây là nguyên nhân phổ biến nhất gây treo node liên quan tới service.
  • Gọi future.result() trước khi future.done() trả về True — truy cập kết quả khi Future chưa hoàn tất gây lỗi hoặc trả về None thay vì response thật — luôn xử lý kết quả trong callback đăng ký qua add_done_callback, không tự ý đọc result() ngay sau khi gọi call_async.
  • Không xử lý exception trong done callback — nếu server gặp lỗi hoặc kết nối bị ngắt giữa chừng, future.result() có thể raise exception — callback xử lý kết quả nên có try/except để tránh crash toàn bộ node vì 1 lần gọi service thất bại.
  • Quên rằng script độc lập (không phải node dài hạn) vẫn cần rclpy.spin_until_future_complete — chỉ gọi call_async rồi đọc kết quả ngay lập tức (không chờ) sẽ luôn nhận None vì response chưa kịp tới.

Kết luận

Hiểu đúng khác biệt giữa gọi service đồng bộ và bất đồng bộ, và biết chính xác khi nào mỗi kiểu an toàn để dùng, là kỹ năng debug quan trọng bậc nhất khi node ROS2 "tự treo" không rõ nguyên nhân. Quy tắc an toàn: luôn dùng call_async bên trong bất kỳ callback nào của node, chỉ dùng gọi đồng bộ khi chắc chắn đang ở ngoài executor. Bài tiếp theo trong chuyên mục nói về Action — cơ chế được ghép từ Service và Topic, thừa hưởng cả ưu điểm lẫn một số cạm bẫy tương tự.

Service: đồng bộ hay bất đồng bộ? Cơ chế bên trong — Atlasbot