The deletion of tasks is the process of removing a task from the Scheduler and freeing the resources that FreeRTOS reserved for it.
So far we have learned to create life in our microcontroller: we spawn tasks, give them memory, and put them to work. But sometimes tasks are not forever.
Think of a task that only serves to initialize complex hardware at startup (configuring a sensor or establishing a WiFi connection) and then becomes useless. Or a configuration mode that, once finished, must disappear to free up RAM.
In these cases, leaving the task in an empty infinite loop or suspended is a waste of resources. The correct thing to do is to delete it. Today we close the task management block by learning to use vTaskDelete and, most importantly, to leave no memory leaks along the way.
The vTaskDelete function
The API for deleting a task is deceptively simple:
void vTaskDelete( TaskHandle_t xTaskToDelete );It only receives one parameter: the Handle of the task we want to destroy.
Self-deletion
This is the most common case. A task finishes its work and decides it is no longer needed. To delete itself, we pass NULL as a parameter.
void SingleUseTask(void *pvParameters) {
// 1. Do the initialization work
Serial.println("Configuring peripherals...");
configurarSensores();
// 2. Create the main task that will continue working
xTaskCreate(MainTask, "Main", 2048, NULL, 1, NULL);
Serial.println("My work here is done. Goodbye.");
// 3. Self-deletion
vTaskDelete(NULL);
// 4. This line will NEVER execute
Serial.println("This never prints");
}When a task is deleted, the memory of its stack and its TCB are not freed in that call. The Idle task is responsible for recovering that RAM, so we must give it CPU time.
Delete another task
One task (e.g., a system administrator) decides to delete another. For this we need to have saved the Handle when creating it.
// Global variable to save the victim's ID
TaskHandle_t hVictim = NULL;
void KillerTask(void *params) {
// ... logic ...
if(userPushesStop()) {
if(hVictim != NULL) {
vTaskDelete(hVictim);
hVictim = NULL; // Good practice: avoid Dangling Pointers
}
}
}The task is no longer schedulable, but the Idle task is still responsible for freeing its TCB and stack. Furthermore, this method is dangerous: the task might have an open file, reserved memory, or an acquired mutex.
Whenever possible, send it a signal to clean up its resources and delete itself. That way it knows the exact point where it ends.
The great danger: cleaning up resources
This is the point where serious errors that lead to Memory Leaks usually appear.
vTaskDelete only initiates the freeing of the memory that the kernel allocated for the task:
- The Stack.
- The TCB (Task Control Block).
But, what about the resources that the task reserved on its own?
Imagine this task:
void LeakyTask(void *params) {
// Reserve dynamic memory on the HEAP
char *buffer = (char*) malloc(1024);
// Open a connection or file (system resource)
WiFiClient client = server.available();
// ... does stuff ...
// SERIOUS ERROR!
vTaskDelete(NULL);
}When we call vTaskDelete:
- The task’s Stack disappears.
- The
bufferpointer (which was on the Stack) disappears. - BUT the 1024-byte block on the Heap is still occupied. Nobody freed it.
- The destructor of
clientis not executed, becausevTaskDeletedoes not perform the normal unwinding of a C++ function.
We just lost 1KB of RAM forever (until reset). If we do this many times, the ESP32 will run out of memory and crash.
The correct way: Clean up before leaving
If a task is going to be deleted, it is the programmer’s responsibility to clean up their “toys” before leaving.
void CleanTask(void *params) {
// Resources
char *buffer = (char*) malloc(1024);
// ... work ...
// Explicit cleanup
if(buffer != NULL) {
free(buffer); // Free the Heap
}
// Now we can leave peacefully
vTaskDelete(NULL);
}Practical example: initialization task
A disposable initialization task can be useful when we want to separate startup from permanent work and recover its stack upon completion.
void SensorTask(void *pvParameters) {
for(;;) {
Serial.println("Reading sensor...");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void SetupTask(void *pvParameters) {
Serial.println("--- Start of Advanced Setup ---");
// Simulate a heavy initialization (e.g., connect WiFi, mount SD)
// that would take 5 seconds.
for(int i=0; i<5; i++) {
Serial.print(".");
vTaskDelay(pdMS_TO_TICKS(1000));
}
Serial.println("\nConfiguration completed.");
// Launch the real application task
xTaskCreate(SensorTask, "Sensor", 2048, NULL, 1, NULL);
Serial.println("Setup Task self-destructing. Freeing 2048 bytes of RAM.");
vTaskDelete(NULL);
}
void setup() {
Serial.begin(115200);
// Only create the setup task
xTaskCreate(SetupTask, "SetupTask", 2048, NULL, 1, NULL);
}
void loop() {
// The loop does not participate in this example, but yields CPU time.
delay(1000);
}