Service: đồng bộ hay bất đồng bộ? Cơ chế bên trong
Atlasbot · 16/8/2026 · 11 phút đọc
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êncall_asynckè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ông | Có | Không, trả Future ngay |
| An toàn khi gọi trong callback | Rủi ro deadlock cao | An 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ơn | Cầ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_completehoặ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 khifuture.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ềNonethay vì response thật — luôn xử lý kết quả trong callback đăng ký quaadd_done_callback, không tự ý đọcresult()ngay sau khi gọicall_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ọicall_asyncrồi đọc kết quả ngay lập tức (không chờ) sẽ luôn nhậnNonevì 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ự.