Exercises#

This page contains two take-home exercises that reinforce the concepts from Lecture 9. Each exercise asks you to write code from scratch based on a specification – no starter code is provided.

All files should be created inside your ~/enpm605_ws/src/executor_demo/ workspace package.

Exercise 1 – Mutex vs Single-Threaded Comparison

Goal

Observe the behavior difference between a SingleThreadedExecutor and a MultiThreadedExecutor with a MutuallyExclusiveCallbackGroup containing a slow callback.


Specification

Create the file executors_demo/slow_callback_demo.py that implements the following.

  1. ``SlowCallbackDemo(Node)`` class:

    • __init__(self): calls super().__init__("slow_cb_demo"), creates a MutuallyExclusiveCallbackGroup stored as _group, and creates three timers:

      • _fast_timer: 2 Hz, 50 ms execution (simulated with time.sleep(0.05)), assigned to _group.

      • _slow_timer: 1 Hz, 600 ms execution (time.sleep(0.60)), assigned to _group.

      • _monitor_timer: 5 Hz, negligible execution, assigned to _group. Logs "monitor tick" without sleeping.

  2. Three callbacks each logging their name and the current wall time at entry and exit.

  3. Two entry points in scripts/:

    • run_single_threaded.py: uses rclpy.spin(node) (implicitly single-threaded).

    • run_multi_threaded.py: uses MultiThreadedExecutor(num_threads=4).

  4. Register both in setup.py.

Observation questions (answer as comments at the top of each entry point file)

  • In the single-threaded case, what happens to _fast_timer and _monitor_timer while _slow_timer is executing?

  • In the multi-threaded case with a mutex group, is there any difference in behavior? Why or why not?

Verification

ros2 run executors_demo single_threaded_slow
ros2 run executors_demo multi_threaded_slow
Exercise 2 – Reentrant Pipeline

Goal

Build a node with two independent callback pipelines using a ReentrantCallbackGroup, observe that they overlap correctly, and introduce a deliberate race condition to see what goes wrong.


Specification

Create executors_demo/reentrant_pipeline.py.

  1. ``ReentrantPipeline(Node)`` class with one ReentrantCallbackGroup:

    • _camera_cb: fires at 5 Hz, takes 80 ms (time.sleep(0.08)). Appends a timestamped entry to a shared list self._log.

    • _lidar_cb: fires at 5 Hz, takes 80 ms (time.sleep(0.08)). Appends a timestamped entry to the same self._log.

    • _report_cb: fires at 1 Hz, prints the length and last three entries of self._log.

  2. Entry point scripts/run_reentrant_pipeline.py using MultiThreadedExecutor(num_threads=4).

  3. Register in setup.py.

Part B – Introduce and fix a race condition

Both _camera_cb and _lidar_cb append to the same list without a lock. Run the node for several seconds and note whether you ever observe a corrupted log (duplicated or missing entries). Then:

  • Protect self._log with a threading.Lock.

  • Verify that the log is now consistent.

Written reflection (include as a comment block at the top of the file)

  • Did you observe a race condition without the lock? Why is list append generally safe in CPython but not guaranteed across all Python implementations?

  • What would happen if you switched the group to MutuallyExclusiveCallbackGroup? Would the race condition disappear? At what cost?