Skip to content

Session 1: Sharing State Safely in Cooperative Systems

Synopsis

Explains shared variables, race-like logic errors in cooperative multitasking, and practical rules for designing predictable async state transitions.

Session Content

Session 1: Sharing State Safely in Cooperative Systems

Duration: ~45 minutes
Audience: Python developers with basic programming knowledge
Platform: Raspberry Pi Pico 2 W
Language: MicroPython
IDE: Thonny


1. Session Goals

By the end of this session, learners will be able to:

  • Explain why shared state can be risky in asynchronous/cooperative systems
  • Identify common race-condition patterns in MicroPython uasyncio programs
  • Use uasyncio.Lock to protect shared resources
  • Apply safe patterns for communication between tasks
  • Build a small Pico 2 W example that reads a sensor-like value and updates an actuator safely

2. Prerequisites

Knowledge

  • Basic Python syntax
  • Functions, variables, lists, dictionaries
  • Familiarity with loops and conditionals

Hardware

  • Raspberry Pi Pico 2 W
  • USB cable
  • Breadboard and jumper wires
  • LED
  • 220Ω resistor
  • Optional: pushbutton, DHT11/DHT22 sensor, or similar simple sensor

3. Development Environment Setup

Thonny Setup

  1. Install Thonny from https://thonny.org
  2. Connect the Raspberry Pi Pico 2 W via USB
  3. In Thonny, open:
  4. Tools > Options > Interpreter
  5. Select MicroPython (Raspberry Pi Pico)
  6. Choose the correct port
  7. Click Stop/Restart backend
  8. Verify the REPL shows the MicroPython prompt: >>>

Useful Files

  • main.py — runs automatically on boot
  • boot.py — startup configuration

Uploading Code

  • Save files directly to the Pico using Thonny
  • Use Run current script to test code
  • Use the REPL for quick checks

4. Theory: Why Shared State Becomes Dangerous

In cooperative asynchronous systems, multiple tasks can appear to run at the same time because each task yields control with await.

If two tasks access the same variable or device state, problems can happen when:

  • One task reads a value while another changes it
  • Two tasks update the same counter
  • One task starts a hardware operation while another interrupts the logic

Common issues

  • Lost updates: one task overwrites another task’s work
  • Inconsistent reads: a task reads half-updated data
  • Device contention: two tasks try to use the same sensor or output at once

Rule of thumb

If more than one task touches the same state, decide:

  • Who owns it?
  • Who may read it?
  • Who may write it?
  • How do tasks coordinate?

5. Safe Patterns for Shared State

Pattern 1: Single Owner Task

One task owns the state, and other tasks communicate with it through messages.

Pattern 2: Lock Protection

If multiple tasks must access a shared resource, use uasyncio.Lock.

Pattern 3: Snapshot Copy

Copy shared data before long processing so the data cannot change mid-operation.

Pattern 4: Event Notification

Use an asyncio.Event to signal that new data is available.


6. Hands-On Exercise 1: Cooperative State Sharing with a Lock

This exercise simulates two tasks updating a shared status string and a shared counter safely.

Wiring

No hardware required for this first exercise.

Code: main.py

import uasyncio as asyncio

# Shared state
shared = {
    "counter": 0,
    "status": "idle"
}

# Protect access to shared state
lock = asyncio.Lock()


async def producer():
    """Simulate a task updating shared state."""
    global shared
    while True:
        async with lock:
            shared["counter"] += 1
            shared["status"] = "updated by producer"
            print("Producer updated shared state:", shared)
        await asyncio.sleep(1)


async def consumer():
    """Simulate a task reading shared state safely."""
    global shared
    while True:
        async with lock:
            snapshot = shared.copy()

        # Process snapshot outside the lock to keep lock duration short
        print("Consumer read snapshot:", snapshot)
        await asyncio.sleep(1.5)


async def main():
    # Start tasks concurrently
    asyncio.create_task(producer())
    asyncio.create_task(consumer())

    # Keep the program running
    while True:
        await asyncio.sleep(10)


try:
    asyncio.run(main())
finally:
    asyncio.new_event_loop()

Expected Output

Producer updated shared state: {'counter': 1, 'status': 'updated by producer'}
Consumer read snapshot: {'counter': 1, 'status': 'updated by producer'}
Producer updated shared state: {'counter': 2, 'status': 'updated by producer'}
Consumer read snapshot: {'counter': 2, 'status': 'updated by producer'}

Discussion

  • The lock prevents simultaneous access.
  • The consumer copies the state, then processes it outside the lock.
  • Short lock usage improves responsiveness.

7. Hands-On Exercise 2: Shared LED State with Safe Access

In this exercise, one task toggles an LED state and another task reports it safely.

Wiring

  • LED anode to GP15 through a 220Ω resistor
  • LED cathode to GND

Code: main.py

import uasyncio as asyncio
from machine import Pin

led = Pin(15, Pin.OUT)

# Shared state
state = {
    "led_on": False
}

lock = asyncio.Lock()


async def led_controller():
    """Toggle the LED state every second."""
    while True:
        async with lock:
            state["led_on"] = not state["led_on"]
            led.value(1 if state["led_on"] else 0)
            print("LED controller set led_on =", state["led_on"])
        await asyncio.sleep(1)


async def status_reporter():
    """Report the current LED state safely."""
    while True:
        async with lock:
            current = state["led_on"]
        print("Status reporter sees led_on =", current)
        await asyncio.sleep(0.5)


async def main():
    asyncio.create_task(led_controller())
    asyncio.create_task(status_reporter())

    while True:
        await asyncio.sleep(10)


try:
    asyncio.run(main())
finally:
    asyncio.new_event_loop()

Expected Output

LED controller set led_on = True
Status reporter sees led_on = True
Status reporter sees led_on = True
LED controller set led_on = False
Status reporter sees led_on = False

Learning Points

  • The LED hardware and shared state are kept in sync.
  • Reads are protected even if they are simple.
  • The lock is held only while necessary.

8. Hands-On Exercise 3: Event-Based Data Sharing

This example shows a better pattern for many producer/consumer cases: one task produces data, another waits for it.

Code: main.py

import uasyncio as asyncio

data_ready = asyncio.Event()
latest_value = None


async def producer():
    """Generate new values periodically."""
    global latest_value
    value = 0
    while True:
        value += 1
        latest_value = value
        print("Producer generated:", latest_value)
        data_ready.set()
        await asyncio.sleep(2)


async def consumer():
    """Wait for new values and process them."""
    global latest_value
    while True:
        await data_ready.wait()
        print("Consumer received:", latest_value)
        data_ready.clear()


async def main():
    asyncio.create_task(producer())
    asyncio.create_task(consumer())

    while True:
        await asyncio.sleep(10)


try:
    asyncio.run(main())
finally:
    asyncio.new_event_loop()

Expected Output

Producer generated: 1
Consumer received: 1
Producer generated: 2
Consumer received: 2
Producer generated: 3
Consumer received: 3

Learning Points

  • Event is useful when one task needs to notify another.
  • The consumer sleeps efficiently until data arrives.
  • This pattern reduces unnecessary polling.

9. Practical Mini Project: Safe Button-Driven LED Control

This mini project combines safe state sharing with a physical input.

Objective

  • A button task updates a shared button_pressed state
  • An LED task reacts to the shared state
  • A logger task prints state changes safely

Wiring

  • Button one side to GP14
  • Button other side to GND
  • Use the internal pull-up resistor in code
  • LED on GP15 with 220Ω resistor to GND

Code: main.py

import uasyncio as asyncio
from machine import Pin

button = Pin(14, Pin.IN, Pin.PULL_UP)
led = Pin(15, Pin.OUT)

state = {
    "button_pressed": False,
    "led_on": False
}

lock = asyncio.Lock()


async def button_monitor():
    """Poll the button and update shared state with debounce handling."""
    last_value = button.value()
    stable_value = last_value
    stable_count = 0

    while True:
        current = button.value()

        if current == last_value:
            stable_count += 1
        else:
            stable_count = 0
            last_value = current

        # Simple debounce: require same reading several times
        if stable_count >= 3 and current != stable_value:
            stable_value = current
            async with lock:
                state["button_pressed"] = (stable_value == 0)
                print("Button state changed:", state["button_pressed"])

        await asyncio.sleep_ms(20)


async def led_task():
    """Turn LED on while button is pressed."""
    while True:
        async with lock:
            pressed = state["button_pressed"]
            state["led_on"] = pressed
            led.value(1 if pressed else 0)
        await asyncio.sleep_ms(20)


async def logger_task():
    """Log the shared state periodically."""
    while True:
        async with lock:
            snapshot = state.copy()
        print("Logger:", snapshot)
        await asyncio.sleep(1)


async def main():
    asyncio.create_task(button_monitor())
    asyncio.create_task(led_task())
    asyncio.create_task(logger_task())

    while True:
        await asyncio.sleep(10)


try:
    asyncio.run(main())
finally:
    asyncio.new_event_loop()

Expected Output

Logger: {'button_pressed': False, 'led_on': False}
Button state changed: True
Logger: {'button_pressed': True, 'led_on': True}
Button state changed: False
Logger: {'button_pressed': False, 'led_on': False}

Notes

  • The button is active-low due to pull-up wiring.
  • Debouncing avoids false triggers.
  • Tasks coordinate through a shared dictionary protected by a lock.

10. Common Mistakes to Avoid

  • Holding a lock for too long
  • Doing heavy processing inside a locked section
  • Updating shared state from multiple tasks without protection
  • Polling too frequently without await
  • Forgetting that await points can interrupt logic flow

11. Session Recap

Key Takeaways

  • Shared state in cooperative async code needs careful design
  • Use locks to protect critical sections
  • Prefer message passing or single-owner patterns when possible
  • Keep locked sections short
  • Use events for notification and coordination

Best Practices

  • Copy shared data before processing it
  • Keep hardware access structured and centralized
  • Avoid unnecessary global writes
  • Use uasyncio primitives intentionally

12. Quick Review Questions

  1. Why can shared state be risky in uasyncio programs?
  2. When should you use uasyncio.Lock?
  3. Why is it better to keep locked sections short?
  4. What is the advantage of using Event over polling?
  5. What is the benefit of the single-owner task pattern?

13. Suggested Follow-Up Exercise

Modify the mini project so that:

  • A second button toggles a mode variable
  • The LED blinks faster in one mode and slower in another
  • All state changes remain protected by a lock
  • A logger task prints both button states and mode every second


Back to Chapter | Back to Master Plan | Next Session