Analisis comparativo de simulacion de funciones asincronicas en micropython utilizando la funcion ticks_ms() y la liberia asyncio.py





Este trabajo se origina en un intercambio de opiniones sobre el tema llevado adelante en el canal de telegrama MicroPython y dado el interes demostrado decidi realizar una pequeña investigación sobre el mismo.

Para empezar voy a definir algunas cuestiones basicas a efectos de unificar criterios al momento de la lectura de de este trabajo:

a.- Procesos Asincronico es aquel que se ejecuta sin necesidad de seguir un orden secuencial estricto, permitiendo que otras tareas se realicen en paralelo sin bloquear la ejecución general del programa o sistema.

b.- Ejecuciones realmente paralelas se realizan por ejemplo con multiprocessing. Que permite un paralelismo real al ejecutar código utilizando para cada tarea distintos núcleos de CPU.

c.- La libreria asyncio la ejecución no es realmente paralela, sino que se gestiona con un único hilo mediante un bucle de eventos. Se alterna entre tareas en puntos donde esperan, permitiendo que otras tareas avancen. Esto se conoce como concurrencia cooperativa o multitarea asincrónica.

d.- La funcion ticks_ms() al proporcionar el tiempo en milisegundos desde que el sistema arrancó permite implementar bucles sin bloqueo mediante "polling" (consulta periódica) con lo que se puede simular una ejecucion paralela.

Para analizar el funcionamiento de ambos procedimientos construi los siguientes programas:


asyncio.py (utilizando la librería asyncio)





        import uasyncio as asyncio
        import utime
        
        # Definir intervalos de tiempo en segundos
        INTERVALO_1 = 0.5
        INTERVALO_2 = 0.5
        INTERVALO_3 = 0.5
        INTERVALO_4 = 0.5
        
        # Tiempo de inicio del programa
        tiempo_inicio = utime.ticks_ms()
        
        def tiempo_transcurrido():
            """Calcula el tiempo transcurrido desde el inicio del programa en milisegundos."""
            return utime.ticks_diff(utime.ticks_ms(), tiempo_inicio)
        
        async def tarea_1():
            while True:
                print(f"Tarea 1 : Tiempo transcurrido: {tiempo_transcurrido()} ms")
                await asyncio.sleep(INTERVALO_1)
        
        async def tarea_2():
            while True:
                print(f"Tarea 2 : Tiempo transcurrido: {tiempo_transcurrido()} ms")
                x = 0
                y = 1
                while x < 5000:
                    x += 1
                    if x == (1000 * y):
                        print(f"{x} : Tiempo transcurrido: {tiempo_transcurrido()} ms")
                        y += 1
                    await asyncio.sleep(0)  # Evitar bloqueo
                await asyncio.sleep(INTERVALO_2)
        
        async def tarea_3():
            while True:
                print(f"Tarea 3 : Tiempo transcurrido: {tiempo_transcurrido()} ms")
                print("Simulando proceso intensivo durante 3 segundos...")
                
                inicio = utime.ticks_ms()
                while utime.ticks_diff(utime.ticks_ms(), inicio) < 3000:
                    await asyncio.sleep(0)  # Cede el control a otras tareas
                
                print(f"Terminada tarea 3 : Tiempo transcurrido: {tiempo_transcurrido()} ms")
                await asyncio.sleep(INTERVALO_3)
        
        async def tarea_4():
            while True:
                print(f"Tarea 4 : Tiempo transcurrido: {tiempo_transcurrido()} ms")
                print("Simulando proceso intensivo durante 5 segundos...")
                
                inicio = utime.ticks_ms()
                while utime.ticks_diff(utime.ticks_ms(), inicio) < 5000:
                    await asyncio.sleep(0)  # Cede el control a otras tareas
                
                print(f"Terminada tarea 4 : Tiempo transcurrido: {tiempo_transcurrido()} ms")
                await asyncio.sleep(INTERVALO_4)
        
        async def main():
            # Ejecutar todas las tareas en paralelo
            await asyncio.gather(tarea_1(), tarea_2(), tarea_3(), tarea_4())
        
        # Iniciar el programa asincrónico
        asyncio.run(main())
               
              

    

ticks_ms.py (utilizando la funcion ticks_ms())





        import utime

        # Definir intervalos de tiempo en milisegundos
        INTERVALO_1 = 500
        INTERVALO_2 = 500
        INTERVALO_3 = 500
        INTERVALO_4 = 500
        
        # Tiempo de inicio del programa
        tiempo_inicio = utime.ticks_ms()
        
        # Variables para almacenar el último tiempo de ejecución de cada tarea
        ultimo_tiempo_1 = tiempo_inicio
        ultimo_tiempo_2 = tiempo_inicio
        ultimo_tiempo_3 = tiempo_inicio
        ultimo_tiempo_4 = tiempo_inicio
        
        def tiempo_transcurrido():
            """Calcula el tiempo transcurrido desde el inicio del programa en milisegundos."""
            return utime.ticks_diff(utime.ticks_ms(), tiempo_inicio)
        
        def tarea_1():
            print(f"Tarea 1 : Tiempo transcurrido: {tiempo_transcurrido()} ms")
        
        def tarea_2():
            print(f"Tarea 2 : Tiempo transcurrido: {tiempo_transcurrido()} ms")
            x = 0
            y = 1
            while x < 5000:
                x += 1
                if x == (1000 * y):
                    print(f"{x} : Tiempo transcurrido: {tiempo_transcurrido()} ms")
                    y += 1    
        
        def tarea_3():
            print(f"Tarea 3 : Tiempo transcurrido: {tiempo_transcurrido()} ms")
            print("Simulando proceso intensivo durante 3 segundos...")
            
            # Simulación de proceso intensivo sin bloquear
            inicio = utime.ticks_ms()
            while utime.ticks_diff(utime.ticks_ms(), inicio) < 3000:
                pass  # Mantiene la CPU ocupada durante 3 segundos sin usar sleep()
        
            print(f"Terminada tarea 3 : Tiempo transcurrido: {tiempo_transcurrido()} ms")
        
        def tarea_4():
            print(f"Tarea 4 : Tiempo transcurrido: {tiempo_transcurrido()} ms")
            print("Simulando proceso intensivo durante 5 segundos...")
            
            # Simulación de proceso intensivo sin bloquear
            inicio = utime.ticks_ms()
            while utime.ticks_diff(utime.ticks_ms(), inicio) < 5000:
                pass  # Mantiene la CPU ocupada durante 5 segundos sin usar sleep()
        
            print(f"Terminada tarea 4 : Tiempo transcurrido: {tiempo_transcurrido()} ms")
        
        while True:
            tiempo_actual = utime.ticks_ms()
        
            if utime.ticks_diff(tiempo_actual, ultimo_tiempo_1) >= INTERVALO_1:
                tarea_1()
                ultimo_tiempo_1 = tiempo_actual  
        
            if utime.ticks_diff(tiempo_actual, ultimo_tiempo_2) >= INTERVALO_2:
                tarea_2()
                ultimo_tiempo_2 = tiempo_actual
        
            if utime.ticks_diff(tiempo_actual, ultimo_tiempo_3) >= INTERVALO_3:
                tarea_3()
                ultimo_tiempo_3 = tiempo_actual
        
            if utime.ticks_diff(tiempo_actual, ultimo_tiempo_4) >= INTERVALO_4:
                tarea_4()
                ultimo_tiempo_4 = tiempo_actual  
        
              
              

    

Ambos ejemplos son identicos en lo que se refiere a las cuatro tareas a realizar:

La primera simplementa da una salida por consola.

La segunda cuenta hasta 5000 e imprime los mutiplos de 1000 menores o iguales a 5000

La tercera simula una tarea que tarda 5 segundos en realizarse

La cuarta simula una tarea que tarda 3 segundos en realizarse

Se corrieron ambos programas y se obtuvieron los siguientes resultados por consola:

Consola



Al analizar los resultados se puede destacar lo siguiente:

1.- En relacion al inicio de las tareas podemos observar que usando la funcion ticks_ms() las tareas 1 y 2 se ejutan de manera casi simultanea al igual que con la librería asyncio lo que es logico puesto que el tiempo de la tarea uno es menor a un segundo.

2.- Con el inicio de la tarea 3 surge la primera diferemcia notable, ya que usando la funcion ticks_ms() tardará 31 segundos en iniciarse, que es lo que dura la tarea 2 en finalizarse mientras que con la librería asyncio se inicia apenas 1 segundo despues de iniciada la tarea 1.

3.- Es interesante observar que con la funcion ticks_ms() las acciones de la tarea 2 se ejecutan consecutivamente a los 6, 12, 18, 24 y 30 milisegundos de haberse iniciado y de manera consecutiva, mientras que usando asyncio las acciones se ejecutan cada 500 milisegundos estando intercaladas con la ejecucion de la tarea uno y con la terminacion de la tarea 3.

4.- En relacion a la secuenciacion vemos que a pesar de los distintos tiempos de duración de cada tarea al usar la funcion ticks_ms() se repite siempre la misma secuencia: tarea 1, tarea 2, tarea 3, tarea 4 y asi sucesivamente, en cambio al usar asyncio en los primeros 30 registros observamos la siguiente secuencia: Tarea 1, Tarea 2, Tarea 3, Tarea 4, Tarea 1, Tarea 1, Tarea 1, Tarea 1, Tarea 1, Tarea 1, Tarea 1, Tarea 3, Tarea 2, Tarea 1, Tarea 1, Tarea 1, Tarea 1, Tarea 4, Tarea 1, Tarea 1, Tarea 3, Tarea 1, Tarea 2, Tarea 1, Tarea 1, Tarea 1, Tarea 1, Tarea 1, Tarea 1, Tarea 3 por lo que podemos decir:

a.- Que comparando ambas secuencias se observa que la tarea 1 usando la funcion ticks_ms()es se realiza la misma cantidad de veces que las otras tareas, en cambo con la libreria asyncio se realiza mchicimas mas veces en el mismo tiempo, lo que nos indica que mientras que la funcion ticks_ms() solo ordena secuencialmente las distintas tareas la librería asyncio hace una ejecucion concurrente de las mismas asmilandose a un paralelismo

b.- Que lo mismo y a la inversa ocurre con la tarea 4 que por ser la mas larga al utilizarse la librería asyncio solo ocurre una vez, mientras que con la la funcion ticks_ms() ocurre la misma cantidad de veces que las otras.

5.- Tambien es interasnte observar que con la la funcion ticks_ms() en los aproximadamente 30 segundos que dura la salida de consola se registro en total la ejecución de 54 registros de tareas, mientras que en el mismo tiempo usando la librería asyncio se ejecutaron 156 registros lo que nos indica la gran cantidad de tiempo muerto que ocurre al usar la funcion ticks_ms().

En conclusión creo que claramente en uso de la librería asyncio es recomendable ante la necesidad de hacer multiples tareas en simultaneo, reservandose en todo caso la funcion ticks_ms() cuando solo debemos correlacionar dos tareas y de muy breve tiempo de ejecución


Quedo disposición de quien quiera hacerme consultas o sugerencias mi correo electronico es carlosvaccaro1960@gmail.com