==================================================== 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. .. dropdown:: Exercise 1 -- Mutex vs Single-Threaded Comparison :icon: gear :class-container: sd-border-primary :class-title: sd-font-weight-bold **Goal** Observe the behavior difference between a ``SingleThreadedExecutor`` and a ``MultiThreadedExecutor`` with a ``MutuallyExclusiveCallbackGroup`` containing a slow callback. .. raw:: html
**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** .. code-block:: console ros2 run executors_demo single_threaded_slow ros2 run executors_demo multi_threaded_slow .. dropdown:: Exercise 2 -- Reentrant Pipeline :icon: gear :class-container: sd-border-primary :class-title: sd-font-weight-bold **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. .. raw:: html
**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?