multitarea-hilos-threading-nanoframework

Multitarea e hilos en nanoFramework

  • 6 min

El threading en nanoFramework es la forma de ejecutar varias tareas de manera concurrente dentro del mismo programa.

Hasta ahora, todos nuestros programas seguían un flujo lineal: “haz A, luego haz B, luego espera un segundo, y vuelve a empezar”.

Pero el mundo real no es lineal. ¿Qué pasa si quieres hacer parpadear un LED de estado cada 500ms para indicar que el sistema está vivo, pero a la vez quieres estar escuchando el puerto serie sin perderte ni un solo carácter?

Si usas Thread.Sleep(500) para el LED dentro del único bucle, ese flujo no podrá atender el puerto durante medio segundo.

La solución es la Multitarea (Multithreading). En esta entrada aprenderemos a usar la clase System.Threading.Thread para dividir nuestro programa en varios trabajadores independientes que cooperan entre sí.

¿Qué es un hilo?

Imagina que tu microcontrolador es una cocina.

  • Single-Thread (Lo que hacíamos antes): Tienes un solo cocinero. Corta cebolla, luego pone el agua a hervir, luego espera mirando la olla… Si está mirando la olla, no puede cortar más cebolla.
  • Multi-Thread: Contratas a varios cocineros. Uno vigila la olla (LED), otro corta cebolla (Sensores) y otro atiende al camarero (Wi-Fi). Todo ocurre “a la vez”.

En nanoFramework, el CLR (la máquina virtual) se encarga de repartir el tiempo del procesador entre los distintos hilos. Aunque el núcleo sea uno solo (en la mayoría de MCUs), el sistema cambia tan rápido entre tareas que da la ilusión de paralelismo total.

Crear el primer hilo

Para usar hilos, necesitamos el espacio de nombres System.Threading.

La estructura es sencilla:

Defines un método void que contiene el código de la tarea.

Creas una instancia de Thread pasándole ese método.

Llamas a .Start() para que empiece a trabajar.

using System;
using System.Diagnostics;
using System.Threading;

namespace MultitareaEjemplo
{
    public class Program
    {
        public static void Main()
        {
            Debug.WriteLine("Inicio del Hilo Principal (Main)");

            // 1. Creamos el hilo y le decimos qué método ejecutar
            // 'TareaParpadeo' es el nombre de la función que definimos abajo
            Thread hiloLed = new Thread(TareaParpadeo);
            
            // 2. Opcional: Darle prioridad (Normal, AboveNormal, Highest...)
            hiloLed.Priority = ThreadPriority.Normal;

            // 3. ¡Arrancar!
            hiloLed.Start();

            // El Hilo Principal sigue a lo suyo
            int contador = 0;
            while (true)
            {
                Debug.WriteLine($"[Main] Trabajando en lógica compleja... {contador++}");
                
                // Pausamos el Main 2 segundos. 
                // ¡Fíjate que el LED seguirá parpadeando aunque el Main duerma!
                Thread.Sleep(2000);
            }
        }

        // Este método se ejecutará en paralelo
        private static void TareaParpadeo()
        {
            Debug.WriteLine("[Hilo LED] Iniciado");
            
            // Aquí iría la configuración del GPIO...
            // GpioPin led = ...
            
            while (true)
            {
                Debug.WriteLine("[Hilo LED] Blink!");
                // led.Toggle();

                // IMPORTANTE: Este Sleep SOLO pausa a este hilo, no al Main
                Thread.Sleep(500); 
            }
        }
    }
}
Copied!

Si ejecutas esto, verás en la consola cómo se mezclan los mensajes de [Main] y [Hilo LED]. ¡Están vivos a la vez!

Pasar datos al hilo

A veces queremos arrancar un hilo con parámetros. Por ejemplo, un hilo que controle un motor y necesite saber el ID del motor.

Para esto usamos un delegado especial. La única limitación es que el método debe aceptar un único parámetro de tipo object.

public static void Main()
{
    // Arrancamos dos hilos usando la misma función, pero con datos distintos
    Thread motor1 = new Thread(ControlMotor);
    motor1.Start(1); // Pasamos el ID 1 como argumento

    Thread motor2 = new Thread(ControlMotor);
    motor2.Start(2); // Pasamos el ID 2 como argumento
}

private static void ControlMotor(object idMotor)
{
    // Hacemos cast (conversión) del objeto al tipo real (int)
    int id = (int)idMotor;

    while(true) 
    {
        Debug.WriteLine($"Controlando Motor {id}");
        Thread.Sleep(1000);
    }
}
Copied!

Condiciones de carrera

Cuando dos hilos intentan modificar la misma variable a la vez, podemos obtener un resultado incorrecto.

Imagina un contador global int total = 0.

  • El Hilo A lee 0, le suma 1 y se prepara para guardar.
  • Justo antes de guardar, el sistema pausa al Hilo A y da paso al Hilo B.
  • El Hilo B lee 0 (porque A aún no guardó), le suma 1 y guarda 1.
  • Vuelve el Hilo A y guarda 1.

Resultado: Se han hecho dos sumas, pero el contador vale 1 en vez de 2. Esto es una Condición de Carrera y produce bugs muy difíciles de detectar.

Proteger una sección con lock

Para evitar esto, usamos la palabra clave lock. El lock garantiza que solo un hilo puede entrar en ese bloque de código a la vez. Si otro llega mientras está ocupado, se espera en la puerta.

Necesitamos un objeto “testigo” (normalmente un object privado) para gestionar el bloqueo.

public class ContadorSeguro
{
    private int _cuenta = 0;
    
    // Objeto candado (testigo)
    private readonly object _bloqueo = new object();

    public void Incrementar()
    {
        // Nadie más puede entrar aquí mientras yo esté dentro
        lock (_bloqueo)
        {
            _cuenta++;
            Debug.WriteLine($"Cuenta actual: {_cuenta}");
            
            // Aquí dentro estamos seguros. Es una zona de "acceso exclusivo".
        }
    }
}
Copied!

Usa lock solo para operaciones rápidas. Si pones un Thread.Sleep(5000) dentro de un lock, bloquearás a todos los demás hilos que necesiten ese recurso, matando el propósito de la multitarea.

Prioridades de los hilos

No todos los hilos son iguales.

  • Mantener la conexión Wi-Fi es crítico (High Priority).
  • Leer un sensor de temperatura cada 10 minutos es secundario (Lowest Priority).

nanoFramework respeta estas prioridades. Úsalas con prudencia, porque un hilo de alta prioridad que no cede tiempo puede dejar sin ejecutar a los demás.

Thread critico = new Thread(TareaWifi);
critico.Priority = ThreadPriority.Highest;

Thread fondo = new Thread(TareaLog);
fondo.Priority = ThreadPriority.Lowest;
Copied!

Buenas prácticas y limitaciones

Los hilos facilitan la concurrencia, pero también consumen recursos:

  1. Memoria RAM: Cada hilo necesita su propia pila de memoria (Stack). Crear un hilo consume RAM inmediatamente. En un ESP32 tienes bastante, pero no crees 100 hilos. Intenta mantenerte por debajo de 5-10 hilos en aplicaciones complejas.
  2. Terminar Hilos: Los hilos mueren cuando su método termina (llega al final de la función). Si tienes un while(true), el hilo vivirá para siempre (que suele ser lo deseado en tareas de fondo).
  3. Depuración: Depurar hilos es difícil porque el error puede depender de “cuándo” se pausa cada uno. Usa Debug.WriteLine incluyendo el nombre o ID del hilo para trazar qué está pasando.