Skip to main content

ros2-executors

Decide when to use ROS 2 callback groups, MultiThreadedExecutor, mutexes, and async vs sync clients — patterns used by stacks like Nav2 and MoveIt. Use when designing node concurrency, fixing deadlocks, protecting shared state, or when the user mentions callback groups, MutuallyExclusive, Reentrant, threading, or spin.

Datos de origen

Repositorio
guilyx/dotfiles
Última actividad en el origen
12 de agosto de 2026 a las 19:39
Idioma detectado de SKILL.md
inglés
Estrellas
0
Forks
0

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
ros2-executors
description
Decide when to use ROS 2 callback groups, MultiThreadedExecutor, mutexes, and async vs sync clients — patterns used by stacks like Nav2 and MoveIt. Use when designing node concurrency, fixing deadlocks, protecting shared state, or when the user mentions callback groups, MutuallyExclusive, Reentrant, threading, or spin.
# ROS 2 Executors, Callback Groups, Thread Safety ## Mental model 1. An **executor** schedules ready callbacks onto threads. 2. A **callback group** constrains *which* of those callbacks may run together. 3. A **mutex** protects *data* when different groups (or non-ROS threads) can touch it concurrently. `MultiThreadedExecutor` **without** extra callback groups is a no-op for concurrency: the node’s **default group is MutuallyExclusive**, so everything still runs one-at-a-time. If you chose MT, you **must** assign groups (or you are lying to yourself). ## Decision tree — when to create a callback group | Situation | What to do | |-----------|------------| | Simple node, no waits in callbacks | Default group + `SingleThreadedExecutor` / `spin(node)`. Done. | | Sync service/action wait **inside** a sub/timer/service callback | **Deadlock** on default/ST. Put **caller** and **client** in different groups (any types) **or** one **Reentrant** group, **and** use `MultiThreadedExecutor`. Prefer **async** instead. | | Two sensors that may process in parallel; each must not re-enter itself | Two **different MutuallyExclusive** groups (not one Reentrant). | | Action/service must handle overlapping instances of the *same* callback | That entity’s group = **Reentrant**. | | Timer reads buffers filled by subscriptions | Groups alone do **not** serialize across groups — use a **mutex** (or put timer+subs in one MutuallyExclusive group and accept no overlap). | | Heavy work (vision, planning) | Do **not** do it in the callback. Bound queue → worker thread / task system; callback only enqueues. | | Component in `component_container` (ST) | No parallel callbacks regardless of groups. Need `component_container_mt` + groups. | | Soft realtime control loop (Nav2-style controller) | Dedicated high-rate timer in its own MutuallyExclusive group; keep it short; optional `SCHED_FIFO` on that thread — never put DDS waits in that loop. | ## Group types (exact semantics) | Type | Same callback overlapping itself? | Different callbacks in group overlapping? | Overlap with *other* groups? | |------|-----------------------------------|---------------------------------------------|------------------------------| | **MutuallyExclusive** | No | No | **Yes always** | | **Reentrant** | **Yes** | **Yes** | **Yes always** | Critical: **different groups never protect each other**. Cross-group shared state ⇒ **you** lock. ## Sync wait rules (official) If any callback does a **synchronous** wait (`spin_until_future_complete`, sync service call, blocking action result): - Caller callback and the **client** must be in **different groups**, **or** both in one **Reentrant** group. - Executor must be able to run the response path (`MultiThreadedExecutor` with enough threads). - If that conflicts with thread-safety needs → **use async only** (always safe w.r.t. this deadlock). ## Patterns (Nav2 / MoveIt / production stacks) **A — Sensor fan-in + control timer (very common)** ``` sub_scan → MutuallyExclusive group A sub_odom → MutuallyExclusive group B control_timer → MutuallyExclusive group C shared buffers protected by std::mutex (or one mutex per buffer) executor: MultiThreadedExecutor(≥3) ``` Nav2-style: costmaps / controllers take sensor data under lock, run a bounded control tick; never call services from the tick sync. **B — Client used from a callback (Nav2 BT / planner services)** - Prefer `async_send_request` + done callback / future polled from BT tick. - If sync is unavoidable: client in group `client_cbg`, BT/timer in `bt_cbg`, MT executor. **C — Action server (MoveIt execute / Nav2 navigate)** - Goal/cancel callbacks: short, MutuallyExclusive (or default). - Execute may be long: often runs as a **thread** or work queue kicked from execute; cancel must be able to run while execute is active → cancel path not blocked by execute’s group (Reentrant execute group **or** execute off-executor + atomic cancel flag). - Reject or preempt a second goal; do not silently pile up. **D — Composition (Nav2 default bringup)** - Whole server in one composable node; container_mt if internal concurrency needed. - Intra-process does not remove the need for groups/mutexes. ## Mutex / data rules - Lock **data**, not the entire ROS API (never call `publish` / wait while holding a lock another callback needs). - Prefer: copy under lock → process unlocked → publish unlocked. - `std::mutex` in callbacks that share state across groups; `recursive_mutex` only if you know you re-enter (usually a design smell). - Atomics for flags (cancel, “new data”) when that is all you share. - Non-ROS threads (drivers, GPU): same rules — treat them like another callback group. ## Executor choice | Executor | Use | |----------|-----| | `SingleThreadedExecutor` | Default; no blocking waits in callbacks | | `MultiThreadedExecutor` | Concurrent groups; set `number_of_threads` ≥ distinct ready groups you care about | | `StaticSingleThreadedExecutor` | Fixed entity set; slightly cheaper | | `EventsExecutor` | Many entities / event-driven (confirm distro) | `rclcpp::spin(node)` ⇒ single-threaded. Production: construct the executor explicitly. ## Anti-patterns - MT executor + only default group (“I enabled multi-thread”). - Sync service call in a subscription on ST executor. - One giant Reentrant group “so nothing deadlocks” — races everywhere. - Holding a mutex across `spin_until_future_complete`. - Doing planning/vision inside a 10–50 Hz control callback. - `spin_once` from inside a callback already being spun. ## Python (`rclpy`) Same group rules. Wall-timer overrun + Reentrant can stack callbacks — avoid. Web/async: dedicated spin thread; never mix `asyncio` event loop and `spin` on one thread without a bridge. ## Debug checklist 1. Is the wait sync and in a callback? → groups/async. 2. Is MT used without extra groups? → add groups or drop MT. 3. Shared buffers across groups? → mutex/copy. 4. Container ST vs MT? 5. Log thread ids in suspect callbacks to see actual overlap.
Ver en GitHub