freertos-eliminacion-tareas-vtaskdelete

Cómo eliminar tareas en FreeRTOS con vTaskDelete

  • 5 min

La eliminación de tareas es el proceso de retirar una tarea del Scheduler y liberar los recursos que FreeRTOS reservó para ella.

Hasta ahora hemos aprendido a crear vida en nuestro microcontrolador: engendramos tareas, les damos memoria y las ponemos a trabajar. Pero, a veces, las tareas no son para siempre.

Piensa en una tarea que solo sirve para inicializar un hardware complejo al arrancar (configurar un sensor o establecer la conexión WiFi) y luego deja de ser útil. O en un modo de configuración que, una vez terminado, debe desaparecer para liberar RAM.

En estos casos, dejar la tarea en un bucle infinito vacía o suspendida es un desperdicio de recursos. Lo correcto es eliminarla. Hoy cerramos el bloque de gestión de tareas aprendiendo a usar vTaskDelete y, lo más importante, a no dejar fugas de memoria por el camino.

La función vTaskDelete

La API para eliminar una tarea es engañosamente sencilla:

void vTaskDelete( TaskHandle_t xTaskToDelete );
Copied!

Solo recibe un parámetro: el Handle (manejador) de la tarea que queremos destruir.

Autoeliminación

Es el caso más común. Una tarea termina su trabajo y decide que ya no es necesaria. Para eliminarse a sí misma, pasamos NULL como parámetro.

void TareaDeUnSoloUso(void *pvParameters) {
    // 1. Hacer el trabajo de inicialización
    Serial.println("Configurando periféricos...");
    configurarSensores();
    
    // 2. Crear la tarea principal que seguirá trabajando
    xTaskCreate(TareaPrincipal, "Main", 2048, NULL, 1, NULL);
    
    Serial.println("Mi trabajo aquí ha terminado. Adiós.");
    
    // 3. Auto-eliminación
    vTaskDelete(NULL); 
    
    // 4. Esta línea JAMÁS se ejecutará
    Serial.println("Esto no sale nunca");
}
Copied!

Cuando una tarea se borra, la memoria de su stack y su TCB no se libera en esa llamada. La tarea Idle se encarga de recuperar esa RAM, así que debemos dejarle tiempo de CPU.

Eliminar otra tarea

Una tarea (ej. un administrador del sistema) decide eliminar a otra. Para esto necesitamos haber guardado el Handle al crearla.

// Variable global para guardar el ID de la víctima
TaskHandle_t hVictima = NULL;

void TareaAsesina(void *params) {
    // ... lógica ...
    if(usuarioPulsaStop()) {
        if(hVictima != NULL) {
            vTaskDelete(hVictima);
            hVictima = NULL; // Buena práctica: evitar Dangling Pointers
        }
    }
}
Copied!

La tarea deja de ser planificable, pero la tarea Idle sigue siendo responsable de liberar su TCB y su stack. Además, este método es peligroso: la tarea puede tener un archivo abierto, memoria reservada o un mutex adquirido.

Siempre que sea posible, envíale una señal para que limpie sus recursos y se elimine a sí misma. Así conoce el punto exacto en el que termina.

El gran peligro: limpiar los recursos

Este es el punto en el que suelen aparecer errores graves que llevan a Memory Leaks (fugas de memoria).

vTaskDelete solo inicia la liberación de la memoria que el kernel asignó a la tarea:

  1. El Stack (la pila).
  2. El TCB (Task Control Block).

Pero, ¿qué pasa con los recursos que la tarea reservó por su cuenta?

Imagina esta tarea:

void TareaConFuga(void *params) {
    // Reserva memoria dinámica en el HEAP
    char *buffer = (char*) malloc(1024); 
    
    // Abre una conexión o archivo (recurso del sistema)
    WiFiClient client = server.available();

    // ... hace cosas ...

    // ¡ERROR GRAVE!
    vTaskDelete(NULL);
}
Copied!

Cuando llamamos a vTaskDelete:

  • El Stack de la tarea desaparece.
  • El puntero buffer (que estaba en el Stack) desaparece.
  • PERO el bloque de 1024 bytes en el Heap sigue ocupado. Nadie lo ha liberado.
  • El destructor de client no se ejecuta, porque vTaskDelete no hace el desenrollado normal de una función C++.

Acabamos de perder 1KB de RAM para siempre (hasta el reinicio). Si hacemos esto muchas veces, el ESP32 se quedará sin memoria y colapsará.

La forma correcta: Limpiar antes de salir

Si una tarea va a ser eliminada, es responsabilidad del programador limpiar sus “juguetes” antes de irse.

void TareaLimpia(void *params) {
    // Recursos
    char *buffer = (char*) malloc(1024);
    
    // ... trabajo ...
    
    // Limpieza explícita
    if(buffer != NULL) {
        free(buffer); // Liberamos el Heap
    }
    
    // Ahora sí, nos vamos tranquilos
    vTaskDelete(NULL);
}
Copied!

Ejemplo práctico: tarea de inicialización

Una tarea de inicialización desechable puede ser útil cuando queremos separar el arranque del trabajo permanente y recuperar su pila al terminar.

void TareaSensor(void *pvParameters) {
  for(;;) {
    Serial.println("Leyendo sensor...");
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}

void TareaSetup(void *pvParameters) {
  Serial.println("--- Inicio del Setup Avanzado ---");
  
  // Simulamos una inicialización pesada (ej. conectar WiFi, montar SD)
  // que tardaría 5 segundos.
  for(int i=0; i<5; i++) {
    Serial.print(".");
    vTaskDelay(pdMS_TO_TICKS(1000)); 
  }
  Serial.println("\nConfiguración completada.");

  // Lanzamos la tarea de aplicación real
  xTaskCreate(TareaSensor, "Sensor", 2048, NULL, 1, NULL);
  
  Serial.println("Tarea Setup autodestruyéndose. Liberando 2048 bytes de RAM.");
  vTaskDelete(NULL);
}

void setup() {
  Serial.begin(115200);
  
  // Solo creamos la tarea setup
  xTaskCreate(TareaSetup, "SetupTask", 2048, NULL, 1, NULL);
}

void loop() {
  // El loop no participa en este ejemplo, pero cede tiempo de CPU.
  delay(1000);
}
Copied!