====== Lab 08 ====== ===== Excercise objectives ===== * Introduce [[https://en.wikipedia.org/wiki/Cooperative_multitasking|cooperative multitasting]] using [[https://docs.micropython.org/en/v1.14/library/uasyncio.html|uasyncio]] module. * Use of the queue to enable communication between tasks ===== Introduction ===== Concurrent programming allows us to run multiple tasks simultaneously in a program. It’s extremely useful if you need to have tasks perform something periodically (e.g. taking a measurement from a sensor) or wait for a function to complete (e.g. waiting for a response from a web page). During these downtimes, you can have the program run another task in order to use the processor more efficiently. Asyncio is a Python library that allows us to create functions called **coroutines** that are capable of pausing to allow another coroutine to run. With asyncio, we can create [[https://en.wikipedia.org/wiki/Cooperative_multitasking|cooperative multitasking]] programs. This is different from [[https://en.wikipedia.org/wiki/Preemption_(computing)|preemptive multitasking]], in which the scheduler can force a task to stop in order to run another task. With many preemptive schedulers, the scheduler runs at a given interval to see if a new thread needs to be run. The scheduler can also run when a thread specifically yields. {{:courses:be2m37mam:labs:ac6c69ba-1ff1-4e5a-9275-50240049de58.jpg?400|}} Full preemptive multitasking is possible in Python with the [[https://docs.python.org/3/library/threading.html|threading]] library. However, Python implements a feature known as the [[https://wiki.python.org/moin/GlobalInterpreterLock|global interpreter lock]] (GIL) to prevent race conditions. While this helps beginners create functioning concurrent programs without running into tricky bugs, it limits the capabilities of multitasking and prevents full optimization of processor utilization. [[https://docs.python.org/3/library/asyncio.html|Asyncio]] allows us to create cooperative multitasking programs where tasks must explicitly give up control of the processor to allow other tasks to run. While this puts the onus on the programmer to make sure the tasks play nicely, it helps prevent race conditions caused by schedulers that can (seemingly randomly) pause a thread to execute another thread. With cooperative multitasking, each task must make sure to yield the processor to allow other tasks to run. {{:courses:be2m37mam:labs:ad878381-74bb-43ef-9b9b-2eb149d179df.jpg?400|}} MicroPython implements a version of asyncio called uasyncio that contains a subset of the functions available in the full asyncio library. In this tutorial, we’ll give a brief example of how to use the uasyncio library to create a cooperative multitasking program. ===== Example 1 ===== This example demonstrates how to run multiple concurent tasks that independently control external peripherals. In addition to the machine module, the uasyncio module will be needed in this example. from machine import Pin import uasyncio from utime import sleep In the Wokwi environment, connect three different LEDs to the Raspberry Pi Pico. If you have a physical sample, don't forget the protection resistors. {{:courses:be2m37mam:labs:screenshot_2024-01-02_at_21-58-31_uasyncio_-_wokwi_esp32_stm32_arduino_simulator.png?400|}} In this example, LEDs are connected as follows: green LED is connected to pin 4, red LED is connected to pin 5 and yellow LED is connected to pin 6. Pins 4, 5 and 6 are configured as OUT. Template diagram is available [[https://wokwi.com/projects/384081414003419137|here]]. led1 = Pin(4, Pin.OUT) # green LED led2 = Pin(5, Pin.OUT) # red LED led3 = Pin(6, Pin.OUT) # yellow LED The function, which will be triggered asychronously in individual tasks, has two arguments: the pin number to which the LED is connected and the blink period in seconds. async def blink(led, period): while True: led.on() await uasyncio.sleep(period) # period in s led.off() await uasyncio.sleep(period) In the main function, which is again asynchronous, three tasks are created that run the differently parameterized blink function. Unlike regular programs that run on microcontrollers, the main function does not contain an infinite loop. Instead, there is an asynchronous call to the sleep_ms function, which ensures that the main function runs for 10 seconds. In practice, this would need to be worked out better, but it is comprehensive for demonstrating the possibility of running asynchronous coroutines. async def main(): uasyncio.create_task(blink(led1, 0.16)) # green LED uasyncio.create_task(blink(led2, 0.33)) # red LED uasyncio.create_task(blink(led3, 0.33)) # orange LED # function main() does not contain endless loop # if will stop after 10s await uasyncio.sleep_ms(10000) uasyncio.run(main()) ===== Example 2 ===== This example demonstrates how two coroutines can communicate using Queue. Project template is available [[https://wokwi.com/projects/384631286976704513|here]]. If the implementation will be on a physical sample, fefore start, head to [[https://github.com/peterhinch/micropython-async/blob/master/v3/primitives/queue.py|queue.py]], click **Raw**, copy the code and paste into new document in Thonny. Click **File > Save As** and save file to Pico. import machine import uasyncio import utime import queue import random This example uses built-in LED, so it is necessary to configure it. led = machine.Pin('LED', machine.Pin.OUT) At the core of the program are two cooperating tasks that transfer information to each other through a queue. The queue originates in the main function and is passed as a parameter to the second coroutine. The main function inserts randomly generated numbers into the queue, the blink function selects these numbers from the queue and controls the blink interval with them. async def blink(q): delay_ms = 0 while True: if not q.empty(): delay_ms = await q.get() led.toggle() await uasyncio.sleep_ms(delay_ms) In the main function it is necessary to make sure that random numbers are not inserted too quickly into the queue, otherwise all available memory would be allocated very soon. Alternatively, it would be possible to use a finite size queue in this case. # Coroutine: entry point for asyncio program async def main(): # Queue for passing messages q = queue.Queue() # Start coroutine as a task and immediately return uasyncio.create_task(blink(q)) # Main loop while True: await q.put(int(1000*random.random())) await uasyncio.sleep_ms(1000) # Start event loop and run entry point coroutine uasyncio.run(main())