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.
``SlowCallbackDemo(Node)`` class:
__init__(self): callssuper().__init__("slow_cb_demo"), creates aMutuallyExclusiveCallbackGroupstored as_group, and creates three timers:_fast_timer: 2 Hz, 50 ms execution (simulated withtime.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.
Three callbacks each logging their name and the current wall time at entry and exit.
Two entry points in
scripts/:run_single_threaded.py: usesrclpy.spin(node)(implicitly single-threaded).run_multi_threaded.py: usesMultiThreadedExecutor(num_threads=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_timerand_monitor_timerwhile_slow_timeris 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.
``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 listself._log._lidar_cb: fires at 5 Hz, takes 80 ms (time.sleep(0.08)). Appends a timestamped entry to the sameself._log._report_cb: fires at 1 Hz, prints the length and last three entries ofself._log.
Entry point
scripts/run_reentrant_pipeline.pyusingMultiThreadedExecutor(num_threads=4).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._logwith athreading.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
appendgenerally 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?