Session 4: Task Cancellation, Timeouts, and Graceful Shutdown
Synopsis
Teaches cancellation handling, timeout patterns, cleanup responsibilities, and controlled shutdown of multitasking applications on embedded hardware.
Session Content
Session 4: Task Cancellation, Timeouts, and Graceful Shutdown
Session Overview
In this session, learners will explore how to stop asynchronous work safely in MicroPython on the Raspberry Pi Pico 2 W. They will learn how to:
- Cancel running tasks
- Use timeouts to prevent indefinite waiting
- Handle
CancelledErrorandTimeoutError - Clean up resources gracefully during shutdown
- Design robust async applications for embedded and IoT systems
This session uses uasyncio in MicroPython and focuses on practical patterns that are essential for reliable Pico 2 W projects.
Learning Outcomes
By the end of this session, learners will be able to:
- Explain why cancellation and timeouts are important in embedded async applications
- Cancel tasks safely and understand how cancellation propagates
- Use timeout patterns to avoid blocking forever
- Build cleanup logic using
try/finally - Implement a graceful shutdown sequence for a Pico 2 W async application
- Apply these patterns to hardware-driven tasks such as LEDs, buttons, and network loops
Prerequisites
- Basic Python knowledge
- Completed understanding of
uasynciotasks and event loops - Raspberry Pi Pico 2 W running MicroPython
- Thonny IDE installed and configured for MicroPython
Development Environment Setup
Thonny Setup
- Install Thonny on your computer.
- Connect the Raspberry Pi Pico 2 W via USB.
- Open Thonny.
- Go to:
- Run > Select interpreter
- Choose MicroPython (Raspberry Pi Pico)
- Select the correct port
- Verify the REPL works by running:
print("Hello from Pico 2 W")
Recommended Files
main.pyfor the final programboot.pyfor startup configuration if needed- Optional helper modules for larger projects
Required Hardware
- Raspberry Pi Pico 2 W
- USB cable
- Breadboard
- 1 LED
- 1 resistor (220–330 ohm)
- 1 push button
- Jumper wires
Suggested Pin Wiring
- LED anode → GP15 through resistor
- LED cathode → GND
- Button one side → GP14
- Button other side → GND
Theory
1. Why Cancellation Matters
Async programs often run multiple tasks at once: - reading sensors - blinking LEDs - watching buttons - maintaining network connections
If one task must stop, it should be cancelled cleanly so the program does not: - leak memory - leave GPIO in an unsafe state - keep sockets or connections open - crash unexpectedly
2. Task Cancellation in uasyncio
A task can be cancelled using:
task.cancel()
When cancelled, the task receives a cancellation exception at its next await point. The task should:
- catch the cancellation if cleanup is needed
- re-raise it or exit cleanly
- release hardware resources in finally
3. Timeouts
Timeouts help prevent waiting forever on: - button presses - network responses - sensor reads - I/O operations
A timeout ensures your application remains responsive, even if hardware or network conditions are poor.
4. Graceful Shutdown
Graceful shutdown means: - stop accepting new work - cancel running tasks - wait briefly for cleanup - turn off actuators - close connections - leave the device in a safe state
This is especially important for IoT devices that may run unattended.
Session Structure
Part 1: Theory and Concepts
- Cancellation lifecycle
- Timeout behavior
- Cleanup with
try/finally - Graceful shutdown strategy
Part 2: Hands-on Exercises
- Exercise 1: Cancel a blinking task with a button
- Exercise 2: Use a timeout while waiting for a button press
- Exercise 3: Build a graceful shutdown controller
Part 3: Wrap-up
- Review patterns
- Common mistakes
- Q&A
Hands-on Exercise 1: Cancel a Blinking Task with a Button
Goal
Create an async LED blinker task and cancel it when a button is pressed.
Learning Focus
- Creating tasks
- Cancelling tasks
- Cleaning up GPIO safely
Code: main.py
from machine import Pin
import uasyncio as asyncio
# --- Hardware setup ---
led = Pin(15, Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_UP)
async def blinker():
"""Blink the LED until the task is cancelled."""
try:
while True:
led.toggle()
print("LED:", led.value())
await asyncio.sleep(0.5)
except asyncio.CancelledError:
# Task was cancelled; make sure the LED is turned off.
print("Blinker cancelled")
led.off()
raise
finally:
# Final safety cleanup
led.off()
print("Blinker cleanup complete")
async def wait_for_button_press():
"""Wait for the button to be pressed."""
while button.value() == 1:
await asyncio.sleep_ms(50)
print("Button pressed")
async def main():
blink_task = asyncio.create_task(blinker())
print("Press the button to stop blinking")
await wait_for_button_press()
print("Cancelling blinker task...")
blink_task.cancel()
try:
await blink_task
except asyncio.CancelledError:
print("Blink task finished after cancellation")
print("Shutdown complete")
try:
asyncio.run(main())
finally:
asyncio.new_event_loop()
Example Output
Press the button to stop blinking
LED: 1
LED: 0
LED: 1
Button pressed
Cancelling blinker task...
Blinker cancelled
Blinker cleanup complete
Blink task finished after cancellation
Shutdown complete
Exercise Instructions
- Wire the LED and button as described above.
- Paste the code into Thonny and save it as
main.py. - Upload and run it on the Pico 2 W.
- Press the button to stop the blinking task.
- Observe that the LED turns off during cleanup.
Questions to Discuss
- What happens if you remove the
finallyblock? - Why is
raiseused after catchingCancelledError? - Why should hardware be turned off during cleanup?
Hands-on Exercise 2: Use a Timeout While Waiting for a Button Press
Goal
Use a timeout so the program does not wait forever for user input.
Learning Focus
- Timeout handling
- Fallback behavior
- Responsiveness in embedded systems
Code: main.py
from machine import Pin
import uasyncio as asyncio
button = Pin(14, Pin.IN, Pin.PULL_UP)
led = Pin(15, Pin.OUT)
async def wait_for_button_press():
"""Wait until the button is pressed."""
while button.value() == 1:
await asyncio.sleep_ms(50)
return True
async def main():
led.off()
print("Waiting for button press for up to 5 seconds...")
try:
# wait_for() raises TimeoutError if the coroutine does not finish in time
await asyncio.wait_for(wait_for_button_press(), 5)
print("Button pressed in time")
led.on()
except asyncio.TimeoutError:
print("Timeout: no button press detected")
led.off()
await asyncio.sleep(2)
led.off()
print("Done")
try:
asyncio.run(main())
finally:
asyncio.new_event_loop()
Example Output
Waiting for button press for up to 5 seconds...
Timeout: no button press detected
Done
If the button is pressed in time:
Waiting for button press for up to 5 seconds...
Button pressed in time
Done
Exercise Instructions
- Run the program without pressing the button.
- Observe the timeout message after 5 seconds.
- Run the program again and press the button quickly.
- Observe the success path.
Questions to Discuss
- Why is a timeout useful in an embedded system?
- What should the device do if a user never presses the button?
- How does this improve reliability?
Hands-on Exercise 3: Graceful Shutdown Controller
Goal
Combine multiple tasks and shut them down cleanly when a stop condition occurs.
Learning Focus
- Coordinating multiple tasks
- Using a shared shutdown flag
- Cleaning up peripherals and tasks
Code: main.py
from machine import Pin
import uasyncio as asyncio
# --- Hardware setup ---
led = Pin(15, Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_UP)
# Shared shutdown flag
shutdown_requested = False
async def status_led():
"""Slow heartbeat LED."""
try:
while not shutdown_requested:
led.on()
await asyncio.sleep_ms(200)
led.off()
await asyncio.sleep_ms(800)
finally:
led.off()
print("Status LED stopped")
async def monitor_button():
"""Request shutdown when the button is pressed."""
global shutdown_requested
while not shutdown_requested:
if button.value() == 0:
print("Shutdown requested")
shutdown_requested = True
break
await asyncio.sleep_ms(50)
async def worker_task():
"""Simulate background work."""
try:
count = 0
while not shutdown_requested:
print("Working:", count)
count += 1
await asyncio.sleep(1)
finally:
print("Worker cleanup complete")
async def main():
global shutdown_requested
print("System started")
tasks = [
asyncio.create_task(status_led()),
asyncio.create_task(monitor_button()),
asyncio.create_task(worker_task()),
]
# Wait until shutdown is requested
while not shutdown_requested:
await asyncio.sleep_ms(100)
print("Cancelling tasks...")
for task in tasks:
task.cancel()
for task in tasks:
try:
await task
except asyncio.CancelledError:
pass
led.off()
print("All tasks stopped safely")
try:
asyncio.run(main())
finally:
asyncio.new_event_loop()
Example Output
System started
Working: 0
Working: 1
Working: 2
Shutdown requested
Cancelling tasks...
Status LED stopped
Worker cleanup complete
All tasks stopped safely
Exercise Instructions
- Load the code into
main.py. - Run the program.
- Observe the heartbeat LED and worker messages.
- Press the button to request shutdown.
- Confirm that all tasks stop and the LED turns off.
Questions to Discuss
- Why is a shared shutdown flag useful?
- What would happen if tasks were not cancelled?
- Why is cleanup done in
finally?
Best Practices
1. Always Clean Up in finally
Use finally blocks to ensure GPIO and other resources are reset even if an error occurs.
2. Cancel Tasks Deliberately
Do not leave background tasks running indefinitely when the program is exiting.
3. Use Timeouts for Risky Waits
Wrap operations that may stall: - waiting for button input - network requests - sensor reads
4. Keep Shutdown Logic Simple
A shutdown path should be easy to understand and test.
5. Turn Actuators Off on Exit
Motors, relays, LEDs, and buzzers should default to a safe state.
Common Mistakes
Mistake 1: Ignoring CancelledError
If cancellation is swallowed without cleanup, tasks may stop in an unsafe state.
Mistake 2: Forgetting to Await Cancelled Tasks
Calling cancel() is not enough. Await the task to allow cleanup to finish.
Mistake 3: Blocking the Event Loop
Using long blocking calls can prevent cancellation from being processed.
Mistake 4: No Timeout on Network or Input
A task waiting forever can make the whole system feel frozen.
Mini-Challenge
Extend the graceful shutdown example so that: - a second LED indicates “running” - the button press triggers a 3-second shutdown delay - the LED blinks faster during shutdown - a message is printed before the final exit
Suggested Approach
- Add a second LED on GP16
- Use a
shutdown_requestedflag - Create a dedicated
shutdown_manager()task - Use
try/finallyin all tasks
Knowledge Check
- What is the difference between cancelling a task and using a timeout?
- Why should you handle
CancelledErrorcarefully? - What is the purpose of
finallyin async tasks? - When would a graceful shutdown be necessary in an IoT device?
- Why is it important to wait for cancelled tasks to finish cleanup?
Summary
In this session, learners practiced: - cancelling async tasks - handling timeouts - writing cleanup-safe code - shutting down embedded async applications gracefully
These patterns are essential for reliable Pico 2 W projects, especially when working with sensors, actuators, and network-connected systems.
Suggested Homework
Create a Pico 2 W program that: - blinks an LED - reads a button - uses a timeout for button response - cancels the LED task on timeout or button press - prints a shutdown summary before exiting