Atlas Robotics
ROS2 nâng caoNâng cao

Executor — cơ chế thực sự đứng sau rclpy.spin()

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

Executor — cơ chế thực sự đứng sau rclpy.spin()
Vòng lặp wait-set → chọn callback sẵn sàng → gọi callback của Executor

Gần như mọi node ROS2 kết thúc bằng rclpy.spin(node), nhưng ít ai để ý dòng này thực chất tạo và chạy một Executor ẩn bên dưới. Hiểu đúng Executor là gì giải thích được cả hai bài trước — vì sao callback mặc định chạy tuần tự (Multi-threading), và vì sao Callback Group kiểm soát được việc song song.

Khái niệm chính

Executor là vòng lặp (event loop) làm đúng ba việc lặp đi lặp lại:

  1. Xây wait-set — hỏi lớp rcl/DDS bên dưới: trong số các subscriber/timer/service/action đã đăng ký với node, cái nào đang có dữ liệu sẵn sàng để xử lý ngay bây giờ?
  2. Chọn callback sẵn sàng — từ danh sách trên, chọn ra các callback đủ điều kiện chạy (còn phụ thuộc Callback Group của từng cái)
  3. Gọi callback — thực thi hàm callback tương ứng, trên luồng nào tuỳ loại Executor (Single hay MultiThreaded)
import rclpy
rclpy.init()
node = rclpy.create_node("my_node")
rclpy.spin(node)   # tạo SingleThreadedExecutor, add_node(), rồi spin() vô hạn

rclpy.spin(node) chỉ là hàm tiện ích gói gọn 3 dòng: tạo executor mặc định, add_node(node), gọi executor.spin() — viết tường minh ba bước này ra là cách duy nhất để tự chọn loại executor khác thay vì mặc định.

Nguyên lý hoạt động

executor = rclpy.executors.SingleThreadedExecutor()
executor.add_node(node)

while rclpy.ok():
    executor.spin_once(timeout_sec=0.1)   # xử lý đúng 1 vòng rồi trả quyền điều khiển
    # có thể chèn code khác ở đây giữa các vòng

spin() chạy vô hạn cho tới khi node bị shutdown — phù hợp phần lớn trường hợp. spin_once() chỉ xử lý một lượt rồi trả lại quyền điều khiển cho vòng lặp while của bạn — dùng khi cần chèn logic tuỳ biến giữa các lượt xử lý callback.

Tóm lại: Executor là lớp trung gian giữa "dữ liệu đã tới từ DDS" và "code callback của bạn thực sự chạy" — nó quyết định thứ tự và mức độ song song, không phải DDS hay rclpy tự làm việc đó.

Kiến thức kỹ thuật

Thông số

So sánh 3 loại Executor có sẵn trong rclpy:

Loại ExecutorĐặc điểm
SingleThreadedExecutorMặc định, 1 luồng, xây lại wait-set mỗi vòng lặp
MultiThreadedExecutorNhiều luồng, cho phép song song nếu Callback Group phù hợp
StaticSingleThreadedExecutor1 luồng, xây wait-set 1 lần duy nhất lúc khởi tạo — nhanh hơn nhưng không hỗ trợ tốt việc thêm/bớt subscriber lúc runtime

Ứng dụng thực tế

StaticSingleThreadedExecutor phù hợp nhất cho node sản xuất (production) có tập hợp publisher/subscriber/timer cố định ngay từ lúc khởi tạo, không đổi trong suốt vòng đời — điển hình của hầu hết node driver/điều khiển trên robot thực tế (không giống lúc phát triển, hiếm khi cần thêm subscriber mới lúc runtime). Trên máy tính nhúng tài nguyên hạn chế, việc tránh chi phí xây lại wait-set mỗi vòng lặp (như SingleThreadedExecutor thường làm) có thể tạo ra khác biệt đáng kể cho node cần vòng lặp điều khiển tần suất rất cao.

Ví dụ thực tế

# Tích hợp ROS2 vào 1 GUI app có event loop riêng (VD: PyQt)
# spin() sẽ chiếm toàn quyền điều khiển — không dùng được ở đây
# Thay vào đó, gọi spin_once() định kỳ qua QTimer của GUI

from PyQt5.QtCore import QTimer

class RosGuiApp:
    def __init__(self, node):
        self.node = node
        self.executor = rclpy.executors.SingleThreadedExecutor()
        self.executor.add_node(node)

        self.timer = QTimer()
        self.timer.timeout.connect(self.spin_ros)
        self.timer.start(50)   # gọi spin_once mỗi 50ms

    def spin_ros(self):
        self.executor.spin_once(timeout_sec=0)
Kết quả: GUI vẫn phản hồi mượt (event loop của Qt không bị chiếm dụng),
đồng thời ROS2 vẫn xử lý callback đều đặn mỗi 50ms — 2 event loop
"chia sẻ" quyền điều khiển thay vì tranh giành nhau.

Đây là ứng dụng thực tế phổ biến nhất của spin_once() — khi ROS2 cần "sống chung" với một framework khác đã có vòng lặp sự kiện riêng của nó.

Lỗi thường gặp

  • Gọi rclpy.spin() rồi thắc mắc code sau đó không chạy — spin() chặn (block) vô hạn cho tới khi node shutdown — mọi code viết sau dòng rclpy.spin(node) trong main() chỉ chạy sau khi node đã dừng hẳn, không phải chạy song song với vòng lặp spin.
  • spin_once(timeout_sec=0) gây busy-waiting, chiếm 100% CPU — timeout bằng 0 khiến executor liên tục kiểm tra không nghỉ — nên dùng timeout dương nhỏ (VD: 0.01) để executor "ngủ" một chút giữa các lần kiểm tra nếu không có gì mới.
  • Timeout quá dài trong vòng lặp tích hợp GUI — nếu spin_once(timeout_sec=1.0) trong khi GUI cần phản hồi nhanh hơn nhiều, giao diện sẽ cảm giác "đứng hình" trong khoảng thời gian đó — chọn timeout đủ ngắn so với tần suất cần phản hồi của phần còn lại của ứng dụng.
  • Chỉ 1 node được spin khi thực sự cần nhiều node — rclpy.spin(node) mặc định chỉ quản lý đúng 1 node — cần tạo executor tường minh và add_node() nhiều lần (đã học ở bài Multi-threading) nếu muốn nhiều node chia sẻ chung 1 executor.

Kết luận

Executor là "động cơ" thực sự đứng sau rclpy.spin() — vòng lặp liên tục xây wait-set, chọn callback sẵn sàng, và gọi chúng theo đúng luật của Callback Group và loại Executor đang dùng. Hiểu đúng cơ chế này giải thích trọn vẹn hành vi của cả Multi-threading lẫn Callback Group đã học ở 2 bài trước. Bài tiếp theo, cũng là bài cuối cùng của toàn bộ category ROS / ROS2, nói về DDS Configuration — tinh chỉnh sâu hơn tầng middleware mà mọi Executor cuối cùng đều phải giao tiếp qua.

Executor — cơ chế thực sự đứng sau rclpy.spin() — Atlasbot