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
uasyncioprograms - Use
uasyncio.Lockto 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
- Install Thonny from https://thonny.org
- Connect the Raspberry Pi Pico 2 W via USB
- In Thonny, open:
- Tools > Options > Interpreter
- Select MicroPython (Raspberry Pi Pico)
- Choose the correct port
- Click Stop/Restart backend
- Verify the REPL shows the MicroPython prompt:
>>>
Useful Files
main.py— runs automatically on bootboot.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
Eventis 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_pressedstate - 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
awaitpoints 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
uasyncioprimitives intentionally
12. Quick Review Questions
- Why can shared state be risky in
uasyncioprograms? - When should you use
uasyncio.Lock? - Why is it better to keep locked sections short?
- What is the advantage of using
Eventover polling? - 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
modevariable - 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
14. Reference Links
- MicroPython Documentation
- MicroPython Wiki
- Raspberry Pi Pico MicroPython Documentation
- Raspberry Pi Pico 2 Documentation