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.

来源信息

仓库
guilyx/dotfiles
最近来源活动
2026年8月12日 19:39
检测到的 SKILL.md 语言
英语
星标
0
分支
0

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
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.
在 GitHub 查看