Skip to content

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 CancelledError and TimeoutError
  • 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 uasyncio tasks and event loops
  • Raspberry Pi Pico 2 W running MicroPython
  • Thonny IDE installed and configured for MicroPython

Development Environment Setup

Thonny Setup

  1. Install Thonny on your computer.
  2. Connect the Raspberry Pi Pico 2 W via USB.
  3. Open Thonny.
  4. Go to:
  5. Run > Select interpreter
  6. Choose MicroPython (Raspberry Pi Pico)
  7. Select the correct port
  8. Verify the REPL works by running:
print("Hello from Pico 2 W")
  • main.py for the final program
  • boot.py for 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

  1. Wire the LED and button as described above.
  2. Paste the code into Thonny and save it as main.py.
  3. Upload and run it on the Pico 2 W.
  4. Press the button to stop the blinking task.
  5. Observe that the LED turns off during cleanup.

Questions to Discuss

  • What happens if you remove the finally block?
  • Why is raise used after catching CancelledError?
  • 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

  1. Run the program without pressing the button.
  2. Observe the timeout message after 5 seconds.
  3. Run the program again and press the button quickly.
  4. 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

  1. Load the code into main.py.
  2. Run the program.
  3. Observe the heartbeat LED and worker messages.
  4. Press the button to request shutdown.
  5. 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_requested flag
  • Create a dedicated shutdown_manager() task
  • Use try/finally in all tasks

Knowledge Check

  1. What is the difference between cancelling a task and using a timeout?
  2. Why should you handle CancelledError carefully?
  3. What is the purpose of finally in async tasks?
  4. When would a graceful shutdown be necessary in an IoT device?
  5. 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


Back to Chapter | Back to Master Plan | Previous Session