Ejecucion multihilo en Raspberry Pi Pico y MicroPython

Una de las cosas interesantes de la Raspberry Pi Pico es que dispone de dos núcleos, lo cual nos permitirá realizar ejecuciones multihilo. Si no sabes lo que es una ejecución multihilo, te diré que es lo que hace tu ordenador y lo que te permite tener varios programas abiertos al mismo tiempo. Obviamente, la Raspberry Pi Pico no tiene un sistema operativo que gestione los ciclos de CPU y por lo tanto sólo permitirá un hilo por núcleo, que en este caso son dos.

Para empezar te diré que no te asustes, porque el funcionamiento es bastante simple y te servirá para muchas cosas. Por poner un ejemplo burdo, podrás tener un núcleo leyendo un sensor continuamente, y en paralelo el otro actuando dependiendo de los datos recibidos por ese sensor. Sé que este ejemplo es simple y desaprovecharías la potencia de la Raspberry Pi Pico, pero es un ejemplo fácil 😛

Ejecutar función en otro núcleo

Para realizar la ejecución multihilo lo primero que tienes que hacer es crear la función que se ejecutará en el otro núcleo. En el siguiente ejemplo puedes ver de forma fácil cómo sería.

import machine
import utime
import _thread

# Configuramos el pin del led interno como salida y lo
# asignamos a interna_led
internal_led = machine.Pin(25, machine.Pin.OUT)

# Función que bloqueará el hilo con un loop while
# el cual simplemente mostrará un mensaje cada segundo
def second_thread():
    while True:
        print("Hello, I'm here in the second thread writting every second")
        utime.sleep(1)

# Función que inicializa la ejecución en el segundo núcleo
# El segundo argumento es una lista o diccionario con los argumentos
# que se pasarán a la función.
_thread.start_new_thread(second_thread, ())

# Segundo loop que bloqueará el hilo principal, y que hará
# que el led interno parpadee cada medio segundo
while True:
    internal_led.toggle()
    utime.sleep(0.25)

Como puedes observar, hemos creado una función llamada second_thread, la cual es un loop while que bloquea la ejecución para siempre. Esta función en un Arduino provocaría que parara la ejecución y se quedara mostrando el mensaje cada segundo para siempre. Sin embargo, al disponer de dos núcleos en las Raspberry Pi Pico, podemos derivar esta función al segundo core con la función _thread.start_new_thread, y continuar con la ejecución del programa. La parte final hará que el led interno parpadee dos veces por segundo al mismo tiempo que el otro hilo muestra un mensaje cada segundo.

Tienes que tener en cuenta que tal y como comenté arriba, el microcontrolador dispone de tan sólo dos núcleos, por lo que no puedes ejecutar de nuevo la función _thread.start_new_thread hasta que termine la primera. En el ejemplo anterior la ejecución es infinita, por lo que si intentamos lanzar la función una segunda vez, recibiremos un error:

Para evitar que esto nos suceda durante la ejecución porque se solapen las ejecuciones (porque una de ellas ha tardado de más por ejemplo), usaremos los semáforos, de los cuales hablaremos en el siguiente apartado.

Semáforos para controlar la ejecución multihilo

Tal y como vimos arriba, la ejecución multihilo puede causar que tu microcontrolador crashee si no se tiene cuidado de no ejecutar más de un hilo al mismo tiempo, lo cual obviamente es poco deseable. Para evitarlo llegan los semáforos al rescate. Los semáforos son objetos, que nos permite parar la ejecución del código hasta que el semáforo es liberado. Esto nos ayudará a tener un control sobre la ejecución multihilo.

Por poner un ejemplo fácil, cogeremos el de arriba y añadiremos unos semáforos:

import machine
import utime
import _thread

# Configuramos el pin del led interno como salida y lo
# asignamos a interna_led
internal_led = machine.Pin(25, machine.Pin.OUT)

# Creamos un semáforo (también llamado bloqueo)
baton = _thread.allocate_lock()

# Función que bloqueará el hilo con un loop while
# el cual simplemente mostrará un mensaje cada segundo
def second_thread():
    while True:
        # Adquirimos el bloqueo del semáforo
        baton.acquire()
        print("Hello, I'm here in the second thread writting every second")
        utime.sleep(1)
        # Liberamos el bloqueo del semáforo
        baton.release()

# Función que inicializa la ejecución en el segundo núcleo
# El segundo argumento es una lista o diccionario con los argumentos
# que se pasarán a la función.
_thread.start_new_thread(second_thread, ())

# Segundo loop que bloqueará el hilo principal, y que hará
# que el led interno parpadee cada medio segundo
while True:
    # Adquirimos el bloqueo del semáforo
    baton.acquire()
    internal_led.toggle()
    utime.sleep(0.25)
    # Liberamos el bloqueo del semáforo
    baton.release()

Como puedes observar en el código, hemos cread un semáforo y lo hemos asignado a la variable baton (testigo en inglés). Dentro de la función second_thread adquirimos el bloqueo y no lo liberamos hasta que termina dicha función. En el hilo principal también intentamos adquirir el bloqueo y esto hace que se quede en espera hasta que esté libre. Una vez que está libre continua con la ejecución del código.

Esto provocará que el led no parpadee en paralelo, acabando con la gracia de la ejecución multihilo, pero bueno, es un ejemplo :P. En realidad te abre varias posibilidades, porque con baton.locked() puedes verificar si el semáforo está bloqueado sin esperar. Por lo tanto puedes lanzar sólo procesos en el segundo núcleo cuando este esté libre, sin bloquear el principal.

Para más información acerca del funcionamiento de _thread, te recomiendo pasarte por la página oficial:

https://docs.python.org/3.7/library/_thread.html#module-_thread

Aunque la verdad, no es que tenga mucho más jejeje.

Como siempre, espero que os haya gustado y que os sirva, y no dudéis en comentar cualquier duda o comentario que tengáis. ¡Un saludo!

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.