Semáforos Mutex y monitores

Semáforos Mutex y monitores

Nuestros sistemas embebidos tienen un conjunto limitado de recursos, ya sean internos (UART, SPI, I2C, USB, ADC, DAC, áreas de memoria (búfers), etc), o externos (LCD, teclado, etc). En programas secuenciales (mono-tarea o de tarea única) prácticamente no existe ningún peligro de que una función que escribe en la UART sea interrumpido por otro; lo normal es que una función escriba (o lea) de la UART de principio a fin. Después, otra función, que en la lógica del programa va después, podrá utilizar a la UART de manera segura.

Sin embargo, la programación concurrente o multi-tarea trae consigo una serie de problemas de los cuales debemos estar conscientes y conocer las diferentes técnicas que existen para resolverlos. Uno de estos problemas tiene que ver con los recursos compartidos. Un recurso compartido es un recurso (como los mencionados) que puede ser utilizado por diversas partes del programa y es en lo que vamos a concentrarnos en esta lección.

Otros problemas asociados con la programación concurrente son: deadlocks (bloqueo mutuo), starvation (inanición), y priority inversion (inversión de prioridad). Ninguno es bueno.

Un problema que se presenta en los programas concurrentes es la competencia entre tareas por un mismo recurso compartido (race condition, en inglés, y condición de carrera, en español). Esto es, mientras una tarea de baja prioridad está utilizando un recurso (la impresión a través de la UART, por ejemplo) el sistema operativo decide que es momento de darle la CPU a una tarea de más alta prioridad, y esta otra tarea también utiliza a la UART imprimiendo sus propios mensajes. Cuando termina le devuelve la CPU a la tarea que había interrumpido para que continúe. La salida de un programa de esta naturaleza se ve así:

La tarea de baja prioridad imprime el texto: «abcd...789«; mientras que la tarea de alta prioridad imprime: «HOLA ... mundo«. Nota la mezcla de los textos. Lo mismo podría suceder con una pantalla LCD (ya me pasó), o con las conversiones del ADC, o con la transmisión de datos por SPI o I2C.

La impresión de texto mezclado puede llegar a ser hasta curiosa; sin embargo, el problema podría ser más grave. Por ejemplo, el convertidor ADC del ATmega328 corrompe las lecturas si a la mitad de una conversión inicias otra (ya me pasó, también). El ADC es un sólo módulo con 6 canales. Si tienes dos o más tareas realizando lecturas independientes en dos o más canales diferentes, entonces corres el peligro de obtener lecturas erróneas, y por lo tanto, nada fiables. Una solución es tener un concentrador para el ADC (le decimos «proxy» en el argót), o utilizar un mutex, lo cual, además de ser más fácil, es el tema de la lección de hoy.

A modo de anécdota te platico sobre uno de mis diseños, la tarjeta tipo PLC UB-PLR328. Cuando la estaba programando enfrenté el siguiente reto, al cual quizás tú también ya te enfrentaste: utilizar I2C para controlar diversos dispositivos. En particular la UB-PLR328 utiliza el canal I2C para 3 cosas: leer la hora de un reloj/calendario MCP79410, escribir a un LCD vía un PCF8574, y leer de un teclado vía otro PCF8574 (aquí puedes ver un video donde muestro los 3 elementos trabajando). Si suponemos que el acceso a cada uno de estos dispositivos es secuencial en un programa mono-tarea, entonces no tendremos preocupaciones mayores, quizás un poco con la lectura y decodificación del teclado. Sin embargo, en un programa multi-tarea sí que debemos preocuparnos y extremar precauciones, ¿qué tal si mientras una tarea quiere leer la hora, otra tarea quiere decodificar al teclado, y otra escribir al LCD? Continúa leyendo para que veas cómo podemos resolver esta situación utilizando mutex‘es.

Una forma de atacar este problema es con la exclusión mutua: evitar que una tarea accese a un recurso compartido mientras éste está siendo utilizado. Para implementar la exclusión mutua usamos semáforos mutex (le estaré diciendo «el mutex» a partir de ahora). El nombre mutex viene del inglés MUTual EXclusion. Los mutex son semáforos binarios con un par de características interesantes que ya veremos más adelante. El funcionamiento de la exclusión mutua es que mientras una tarea posea al mutex ninguna otra tarea podrá accesar al recurso compartido. Una vez que la tarea que tenía al mutex lo devuelve, entonces la otra tarea que esperaba por él lo obtendrá y podrá accesar al recurso. Hoy vamos a estudiar los mutex y las funciones de semáforos de FreeRTOS.

Además, y como agradecimiento a tu apoyo para la realización de este curso, vamos a estudiar brevemente a los monitores, los cuales son una versión de alto nivel para implementar la exclusión mutua. Veremos sus características y su implementación como clases de C++.

Tabla de contenidos

¿Qué es y qué necesito?

Exclusión mutua y semáforos mutex

La exclusión mutua se implementa a partir de identificar las secciones críticas del programa; esto es, encontrar los lugares donde, para el problema que nos concierne, están ubicados los accesos al recurso compartido y convertirlas en operaciones atómicas (operaciones que no pueden ser interrumpidas hasta terminar). Para el ejemplo de la UART (y el servicio asociado Serial.print() de Arduino) el pseudocódigo de una sección crítica se vería así:

Tarea()
{
   While( 1 )
   {
      // código ...

      critical_section_enter()       // entramos a la sección crítica
      Serial.println( "Hola mundo" ) // usamos al recurso como una operación atómica
      critical_section_exit()        // salimos de la sección crítica

      // ... más código
   }
}

La entrada y salida de una sección crítica puede ser implementada de diversas formas, incluyendo desactivar las interrupciones mientras el recurso es utilizado. Afortunadamente nosotros no debemos hacer eso; en su lugar utilizaremos semáforos mutex.

Un semáforo mutex es un semáforo binario con dos características especiales:

  1. El mutex puede heredar prioridades de manera temporal. Digamos que una tarea de baja prioridad, TL, posee al mutex, y mientras esto sucede una tarea de mayor prioridad, TH, desea adquirlo. Bajo estas condiciones el sistema operativo eleva la prioridad de TL al mismo nivel que TH de manera temporal. De esta forma se asegura hasta cierto punto, que TL devolverá el mutex lo más pronto posible para que TH lo utilice. Cuando TL devuelve el mutex su prioridad es restablecida. Los semáforos binarios no tienen esta característica.
  2. El mutex debe ser devuelto. La tarea que posee al mutex debe devolverlo tan pronto como termine su trabajo para que otras tareas lo puedan obtener, y en consecuencia, accesar al recurso. No es un requisito que los semáforos deban ser devueltos, de hecho, casi nunca se hace.

RECUERDA: Mientras un semáforo binario se utiliza para sincronización y señalización (por ejemplo, una ISR lo da de manera constante, mientras que una tarea lo obtiene también de manera constante), los mutex’es se utilizan para proteger recursos compartidos de ser accesados por dos o más tareas al mismo tiempo.

El pseudocódigo anterior, utilizando mutex’es, nos quedaría así:

Tarea()
{
   While( 1 )
   {
      // código ...

      take( mutex )                  // obtiene al mutex
      Serial.println( "Hola mundo" ) // usa al recurso
      give( mutex )                  // devuelve al mutex
      
      // ... más código
   }
}

Si el mutex no estuviera disponible cuando se hace la llamada take(), entonces el sistema operativo manda a la tarea al estado Blocked y la inserta en una cola de espera. Cuando el recurso es liberado a través de la llamada a give(), entonces el sistema operativo saca de la cola de espera a la tarea que más tiempo ha estado esperando por el recurso.

Un poco más adelante veremos las funciones de FreeRTOS para semáforos mutex. A continuación abordaré brevemente el tema de los monitores, los cuales harán a nuestros programas más seguros.

Monitores

Un monitor es un tipo abstracto que encapsula al recurso y al mutex, y expone las únicas operaciones que pueden ser realizadas sobre el recurso.

En la descripción dada sobre mutex’es, y en general, sobre semáforos, notarás que estos no tienen dueño ni control sobre quién lo da y quién lo obtiene. Este es un problema «normal» con los semáforos: cualquier tarea podría obtenerlo y no devolverlo, o utilizarlo sobre un recurso diferente para el que fue creado.

Una forma de enfrentar estos problemas es encapsulando en una misma entidad (tipo abstracto, clase) al mutex junto con el recurso, y exponer un conjunto básico de operaciones que serán las únicas que podrán accesar al recurso. De esta forma sería muy difícil utilizar de manera incorrecta al recurso, o aplicarle el mutex a un recurso diferente.

En los ejemplos de mutex’es que veremos más adelante el recurso a proteger es la impresión serial a través de la función Serial.print(), y podremos usar a esta función de todas las maneras posibles que necesitemos. Pero cuando implementemos al monitor solamente dos operaciones estarán disponibles: escribir una cadena de texto completa, y escribir una cadena de texto completa caracter por caracter. El cliente del monitor no puede hacer nada más, dado que esas son las únicas operaciones expuestas, y siempre operarán sobre el mismo recurso. Por otro lado, podría ser que el cliente ni siquiera se entere que dentro del monitor existe un mutex (ventajas de los tipos abstractos); o podría suceder que el cliente vea al monitor como si fuese el propio recurso (esto sería lo más deseable). Aunque más adelante veremos a detalle la implementación del monitor, te quiero presentar las únicas dos operaciones que decidí sobre el recurso serial:

class Monitor
{
private:

   // declaración del mutex y el recurso
   
public:

   // aquí va el constructor

   // estas son las únicas operaciones permitidas sobre el recurso:

   bool PrintAll(      const char* text );
   bool PrintOneByOne( const char* text, uint8_t len );
};

Mutex’es de FreeRTOS

Para usar mutex’es necesitas utilizar las primitivas de FreeRTOS de semáforos. En lecciones anteriores vimos cómo implementar semáforos binarios y semáforos contadores con un mecanismo nuevo y de bajo consumo de recursos de FreeRTOS: las notificaciones directas a la tarea. Sin embargo, por las características especiales ya mencionadas de los mutex’es sólo hay una forma de implementación en FreeRTOS y es utilizando su API primitiva de semáforos. Para ello tienes que activar esta funcionalidad en el archivo FreeRTOSConfig.h:

#define configUSE_MUTEXES 1

Y también tienes que incluir en tus códigos fuente el archivo de encabezado #include <semphr.h>. Como dato interesante debes tener en cuenta que las primitivas de semáforos de FreeRTOS utilizan colas, por lo cual el tamaño de los programas puede crecer un poco.

Desarrollo

Para usar mutex’es debes crearlos, y como siempre, hay dos formas de hacerlo, de manera dinámica y de manera estática. Si no recuerdas la diferencia entre una y otra, sus ventajas y desventajas, entonces puedes revisar esta lección y esta otra. Comencemos con la primera ya que es la más fácil:

Creación dinámica de mutex’es

Esta función no tiene argumentos.

Valor devuelto: Handler al semáforo recién creado. Antes de usarlo deberías verificar que realmente se creó. (NOTA: Aunque las funciones para manipular los mutex son las mismas que las de los semáforos binarios y contadores de la API primitiva de semáforos, cuida de no mezclarlos.)

Para crear un mutex dinámico puedes hacer lo siguiente:

#include <FreeRTOS.h>
#include <task.h>
#include <semphr.h>

SemaphoreHandle_t g_mutex = NULL;
// debe ser global

void setup()
{
   // código ...

   g_mutex = xSemaphoreCreateMutex();

   configASSERT( g_mutex );
   // error creando al mutex

   // ... más código
}

Creación estática de mutex’es

pxMutexBuffer. Puntero a una variable StaticSemaphore_t que mantendrá el estado del semáforo. Esta variable deberá existir durante toda la vida del programa, por lo cual deberás crearla de manera global o estática a la función donde fue llamada xSemaphoreCreateMutexStatic().

Valor devuelto: Handler al semáforo recién creado. Los objetos estáticos siempre son creados. (NOTA: Aunque las funciones para manipular los mutex son las mismas que las de los semáforos binarios y contadores de la API primitiva de semáforos, cuida de no mezclarlos.)

Para crear un mutex estático puedes hacer lo siguiente:

#include <FreeRTOS.h>
#include <task.h>
#include <semphr.h>

SemaphoreHandle_t g_mutex = NULL;
// debe ser global

void setup()
{
   // código ...

   static StaticSemaphore_t* mutex_buffer;
   // variable de estado del mutex
   
   g_mutex = xSemaphoreCreateMutexStatic( &mutex_buffer );

   // la creación estática nunca falla

   // ... más código
}

En este ejemplo, como en muchos otros donde creamos objetos estáticos, las variables de estado y los arreglos necesarios los hemos creado marcándolos como static. Gracias a esto la variable mutex_buffer existirá durante todo el programa, y al mismo tiempo estará oculta al resto de las funciones. Por supuesto que podrías declararla global (como con g_mutex), pero entre menos variables globales tengas, mejor. g_mutex debe ser global porque tiene que ser accesada por varias tareas; la variable de estado mutex_buffer, no. (En el Ejemplo 4 veremos una forma para que g_mutex deje de ser global, ya sea que uses monitores o no.)

Dando y obteniendo al mutex

Una vez que el mutex ha sido creado lo podemos utilizar con las funciones para darlo y obtenerlo, xSemaphoreGive() y xSemaphoreTake(), respectivamente.

xSemaphore. Handler del mutex.

xTicksToWait. Tiempo de espera, en ticks, para obtener el mutex. Para convertir milisegundos a ticks puedes utilizar la macro pdMS_TO_TICKS(). Para no esperar puedes escribir 0; para un tiempo de espera infinito escribe portMAX_DELAY (asegúrate que la constante INCLUDE_vTaskSuspend, en el archivo FreeRTOSConfig.h, está puesta a 1).

xSemaphore. Handler del mutex.

Valor devuelto. pdTRUE si el mutex fue devuelto; pdFALSE si el mutex no pudo ser devuelto. Esto podría suceder porque el mutex no fue obtenido de manera correcta en primer lugar.

Ejemplos

Vamos a ver 4 ejemplos. Los primeros dos tratan sobre la creación y uso de los mutex «en crudo» (uno con memoria dinámica y otro con memoria estática). El tercer ejemplo muestra la implementación simple, pero efectiva, de un monitor. El cuarto ejemplo es una versión mejorada y más segura del tercer ejemplo.

Ejemplo 1: Mutex dinámico

Veamos un ejemplo completo con mutex dinámico. El programa trata de lo siguiente: el recurso a proteger es la transmisión al puerto serial a través de la función Serial.print(). Hay dos tareas, una con prioridad más alta que la otra (a_task y another_task, respectivamente), y ambas imprimen una cadena larga. A la tarea con baja prioridad le puse un truco para que se tomara mucho tiempo en escribir y pudiera ser interrumpida a la mitad por la tarea con más prioridad.

Al programa le agregué un mecanismo para activar/desactivar el mutex. En un programa normal esto no es necesario, ni deseable, pero lo hice para que puedas ver los resultados con y sin mutex.

#include <FreeRTOS.h>
#include <task.h>
#include <semphr.h>


SemaphoreHandle_t g_mutex = NULL;
// debe ser global


#define MUTEX_EN 0
// Habilita/deshabilita al mutex
// 0: Deshabilitado
// 1: Habilitado

void a_task( void* pvParameters )
{
   char str[66] = "HOLA MUNDO hola mundo HOLA MUNDO hola mundo HOLA MUNDO hola mundo";

   while( 1 )
   {
      vTaskDelay( pdMS_TO_TICKS( 50 ) );

#if MUTEX_EN == 1
      if( xSemaphoreTake( g_mutex, pdMS_TO_TICKS( 100 ) ) != pdFALSE ){
      // toma el mutex; si no está disponible, entonces la tarea se va al estado Blocked
#endif
        
         Serial.println( str );

#if MUTEX_EN == 1
         xSemaphoreGive( g_mutex );
         // devuelve el mutex

      } else{ // timeover:
         Serial.println( "MTX(W)" );
      }
#endif
   }
}

void another_task( void* pvParameters )
{
   char str[63] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

   while( 1 )
   {
      vTaskDelay( pdMS_TO_TICKS( 47 ) );

#if MUTEX_EN == 1 // Con mutex:

      if( xSemaphoreTake( g_mutex, pdMS_TO_TICKS( 100 ) ) != pdFALSE ){
      // toma el mutex; si no está disponible, entonces la tarea se va al estado Blocked

         for( uint8_t i = 0; i < 62; ++i ){
            Serial.print( str[ i ] );
            vTaskDelay( pdMS_TO_TICKS( 1 ) );
         }
         Serial.println( "" );


         xSemaphoreGive( g_mutex );
         // devuelve el mutex

      } else{ // timeover:
         Serial.println( "MTX(R)" );
      }

#else            // Sin mutex:

      for( uint8_t i = 0; i < 62; ++i ){
         Serial.print( str[ i ] );
         vTaskDelay( pdMS_TO_TICKS( 1 ) );
      }
      Serial.println( "" );

#endif
   }
}

void setup()
{
   xTaskCreate( a_task, "PROD", 128 * 3, NULL, tskIDLE_PRIORITY + 1, NULL );

   xTaskCreate( another_task, "CONS", 128 * 3, NULL, tskIDLE_PRIORITY, NULL );

   g_mutex = xSemaphoreCreateMutex();

   configASSERT( g_mutex );
   // error creando al mutex


   pinMode( 13, OUTPUT );

   Serial.begin( 115200 );

   Serial.println( "\n*** RESET ***" );

   vTaskStartScheduler();
}

void loop() {}

RECUERDA: Cuando la tarea termina con el recurso DEBE devolver al mutex. En las líneas 31 y 61 ambas tareas lo devuelven.

Podrás notar diferencias en la forma de activar/desactivar el mutex en cada una de las tareas. En la primera lo hice de tal modo que el código no se repitiera, mientras que en la segunda repetí el código. Lo hice así para que conozcas ambas formas; aunque en la segunda es más claro el uso del mutex, entre menos código copies y pegues, mejor.

La salida de este programa SIN mutex es así:

Programa sin mutex. La salidas de cada tarea se mezclan.

La salida de este programa CON mutex es así:

Programa con mutex. Cada tarea imprime correctamente.

Ejemplo 2: Mutex estático

Este ejemplo es casi idéntico al anterior, tanto que no lo voy a repetir, solamente voy a poner el código de la función setup() que es donde se crea al mutex estático. Todo lo demás se mantiene sin cambios:

void setup()
{
   xTaskCreate( a_task, "1T", 128 * 3, NULL, tskIDLE_PRIORITY + 1, NULL );

   xTaskCreate( another_task, "2T", 128 * 3, NULL, tskIDLE_PRIORITY, NULL );


   static StaticSemaphore_t mutex_buffer;
   // variable de estado del mutex
   
   g_mutex = xSemaphoreCreateMutexStatic( &mutex_buffer );

    // la creación estática nunca falla


   pinMode( 13, OUTPUT );

   Serial.begin( 115200 );

   Serial.println( "\n*** RESET ***" );

   vTaskStartScheduler();
}

Ejemplo 3: Monitor

En este ejemplo vamos a ver una versión simple, pero funcional y efectiva, de un monitor. El recurso a proteger sigue siendo la salida serial, y el monitor está implementado como un tipo abstracto a través de una clase de C++. Ambos, el mutex y el recurso deben ser variables miembro de la clase, sin embargo, para este ejemplo particular el recurso que estamos protegiendo está declarado a nivel global dentro de la arquitectura de Arduino, por lo cual no aparece declarado en la clase, pero en cualquier otro caso el recurso debería ser declarado como variable miembro de la clase que implementa al monitor, y de ser necesario, inicializarlo en el constructor:

class Monitor
{
private:
   SemaphoreHandle_t mutex{ NULL };

   // Resource resource;
   // aquí iría la declaración del recurso, pero Serial.print() es global al
   // sistema, así que sólo supondremos que lo hacemos

public:
   Monitor();

   bool PrintAll( const char* text );
   bool PrintOneByOne( const char* text, uint8_t len );
};

El monitor expone dos operaciones: PrintAll() y PrintOneByOne(). Estas son las únicas operaciones que se pueden realizar sobre el recurso. Además, por supuesto, está el constructor:

Monitor::Monitor()
{
   this->mutex = xSemaphoreCreateMutex();

   // deberíamos verificar que el mutex fue creado ... o mejor aún, crearlo de
   // manera estática

   // si el recurso necesita ser creado o inicializado, aquí lo haríamos
}

bool Monitor::PrintAll( const char* text )
{
   bool retVal{ false };

   if( xSemaphoreTake( this->mutex, pdMS_TO_TICKS( 100 ) ) != pdFALSE ){

      Serial.println( text );
      // recurso compartido: imprime el texto en una sola pasada

      xSemaphoreGive( this->mutex );

      retVal = true;
   } 

   return retVal;
}

bool Monitor::PrintOneByOne( const char* text, uint8_t len )
{
   bool retVal{ false };

   if( xSemaphoreTake( this->mutex, pdMS_TO_TICKS( 100 ) ) != pdFALSE ){

      // recurso compartido: imprime el texto caracter por caracter:
      for( uint8_t i = 0; i < len; ++i ){
         Serial.print( text[ i ] );
         vTaskDelay( pdMS_TO_TICKS( 1 ) );
      }
      Serial.println( "" );

      xSemaphoreGive( this->mutex );

      retVal = true;
   }

   return retVal;
}

Ambas operaciones devuelven un booleano para indicar si la operación se llevó a cabo (true), o si el tiempo expiró y el mutex no pudo ser adquirido (false). El tratamiento de esta última situación se maneja por fuera de la clase. En un sistema con más recursos las operaciones podrían lanzar una excepción (throw) y la tarea llamadora capturarla (catch) e intentar recuperarse.

Nota la simplicidad y seguridad para utilizar el recurso a través del monitor:

void a_task( void* pvParameters )
{
   char str[66] = "HOLA MUNDO hola mundo HOLA MUNDO hola mundo HOLA MUNDO hola mundo";

   while( 1 )
   {
      vTaskDelay( pdMS_TO_TICKS( 50 ) );

      if( monitor.PrintAll( str ) == false ){ // time over:
         Serial.println( "TO:1" );
      }
   }
}

A continuación tenemos el ejemplo completo:

#include <FreeRTOS.h>
#include <task.h>
#include <semphr.h>

/*
 * El mutex queda encapsula en la clase y el recurso sólo puede ser utilizado
 * por las operaciones definidas
 */
class Monitor
{
private:
   SemaphoreHandle_t mutex{ NULL };

   // aquí iría la declaración del recurso, pero Serial.print() es global al
   // sistema, así que sólo supondremos que lo hacemos

public:
   Monitor();

   bool PrintAll( const char* text );
   bool PrintOneByOne( const char* text, uint8_t len );
};

Monitor::Monitor()
{
   this->mutex = xSemaphoreCreateMutex();

   // deberíamos verificar que el mutex fue creado ... o mejor aún, crearlo de
   // manera estática

}

bool Monitor::PrintAll( const char* text )
{
   bool retVal{ false };

   if( xSemaphoreTake( this->mutex, pdMS_TO_TICKS( 100 ) ) != pdFALSE ){

      Serial.println( text );

      xSemaphoreGive( this->mutex );

      retVal = true;
   } 

   return retVal;
}

bool Monitor::PrintOneByOne( const char* text, uint8_t len )
{
   bool retVal{ false };

   if( xSemaphoreTake( this->mutex, pdMS_TO_TICKS( 100 ) ) != pdFALSE ){

      for( uint8_t i = 0; i < len; ++i ){
         Serial.print( text[ i ] );
         vTaskDelay( pdMS_TO_TICKS( 1 ) );
      }
      Serial.println( "" );

      xSemaphoreGive( this->mutex );

      retVal = true;
   }

   return retVal;
}

Monitor monitor;
// el monitor es global para que las tareas tengan acceso a él. Una manera más
// segura es crearlo en setup() (marcado como static) y pasárselo a las tareas
// que van a ocupar el recurso en su parámetro pvParameters


void a_task( void* pvParameters )
{
   char str[66] = "HOLA MUNDO hola mundo HOLA MUNDO hola mundo HOLA MUNDO hola mundo";

   while( 1 )
   {
      vTaskDelay( pdMS_TO_TICKS( 50 ) );

      if( monitor.PrintAll( str ) == false ){ // time over:
         Serial.println( "TO:1" );
      }
   }
}

void another_task( void* pvParameters )
{
   char str[63] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

   while( 1 )
   {
      vTaskDelay( pdMS_TO_TICKS( 47 ) );

      if( monitor.PrintOneByOne( str, 62 ) == false ){ // time over:
         Serial.println( "TO:2" );
      }
   }
}

void setup()
{
   xTaskCreate( a_task, "1T", 128 * 3, NULL, tskIDLE_PRIORITY + 1, NULL );

   xTaskCreate( another_task, "2T", 128 * 3, NULL, tskIDLE_PRIORITY, NULL );

   pinMode( 13, OUTPUT );

   Serial.begin( 115200 );

   Serial.println( "\n*** RESET ***" );

   vTaskStartScheduler();
}

void loop() {}

Ejemplo 4: Monitor mejorado

El siguiente programa incluye algunas mejoras con respecto del Ejemplo 3:

  • El mutex es creado de manera estática con xSemaphoreCreateMutexStatic().
  • El monitor deja de ser global; ahora se declara como static en la función setup() y se le pasa a las tareas que van a utilizar al recurso compartido a través del argumento pvParameters.

Los objetos estáticos son mejores que los dinámicos porque siempre se crean, así que no hay necesidad de preocuparse por ello, ni ninguna razón para liarse con memoria dinámica. Por otro lado, en el Ejemplo 3 el monitor puede ser utilizado por cualquier tarea; en la versión que estoy a punto de mostrarte solamente las tareas que reciban al monitor podrán utilizarlo. Con esto agregamos un escalón más en la seguridad y fiabilidad de nuestros programas.

Como ya hemos visto todo lo que necesitamos para implementar al monitor en la forma que he descrito, vámonos directamente al código:

#include <FreeRTOS.h>
#include <task.h>
#include <semphr.h>

/*
 * El mutex queda encapsula en la clase y el recurso sólo puede ser utilizado
 * por las operaciones definidas
 */
class Monitor
{
private:
   SemaphoreHandle_t mutex{ NULL };

   // aquí iría la declaración del recurso, pero Serial.print() es global al
   // sistema, así que sólo supondremos que lo hacemos

   StaticSemaphore_t mutex_buffer;
   // variable de estado del mutex. Se usa únicamente en la función
   // xSemaphoreCreateMutexStatic()

public:
   Monitor();

   bool PrintAll( const char* text );
   bool PrintOneByOne( const char* text, uint8_t len );
};

Monitor::Monitor()
{
   this->mutex = xSemaphoreCreateMutexStatic( &this->mutex_buffer );
   // los objetos estáticos siempre se crean
}

bool Monitor::PrintAll( const char* text )
{
   bool retVal{ false };

   if( xSemaphoreTake( this->mutex, pdMS_TO_TICKS( 100 ) ) != pdFALSE ){

      Serial.println( text );

      xSemaphoreGive( this->mutex );

      retVal = true;
   } 

   return retVal;
}

bool Monitor::PrintOneByOne( const char* text, uint8_t len )
{
   bool retVal{ false };

   if( xSemaphoreTake( this->mutex, pdMS_TO_TICKS( 100 ) ) != pdFALSE ){

      for( uint8_t i = 0; i < len; ++i ){
         Serial.print( text[ i ] );
         vTaskDelay( pdMS_TO_TICKS( 1 ) );
      }
      Serial.println( "" );


      xSemaphoreGive( this->mutex );

      retVal = true;
   }

   return retVal;
}

void a_task( void* pvParameters )
{
   Monitor* monitor = (Monitor*) pvParameters;
   // aceptamos al monitor

   char str[66] = "HOLA MUNDO hola mundo HOLA MUNDO hola mundo HOLA MUNDO hola mundo";

   while( 1 )
   {
      vTaskDelay( pdMS_TO_TICKS( 50 ) );

      if( monitor->PrintAll( str ) == false ){ // time over:
         Serial.println( "TO:1" );
      }
   }
}

void another_task( void* pvParameters )
{
   Monitor* monitor = (Monitor*) pvParameters;
   // aceptamos al monitor

   char str[63] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

   while( 1 )
   {
      vTaskDelay( pdMS_TO_TICKS( 47 ) );

      if( monitor->PrintOneByOne( str, 62 ) == false ){ // time over:
         Serial.println( "TO:2" );
      }
   }
}

void setup()
{
   static Monitor monitor;
   // el monitor es local a setup(), pero 'static' logra que exista durante todo el programa, 
   // y al mismo tiempo nadie fuera de esta función lo verá

   xTaskCreate( a_task, "1T", 128 * 3, (void*) &monitor, tskIDLE_PRIORITY + 1, NULL );
   // solamente las tareas que reciban al monitor podrán utilizarlo

   xTaskCreate( another_task, "2T", 128 * 3, (void*) &monitor, tskIDLE_PRIORITY, NULL );
   // solamente las tareas que reciban al monitor podrán utilizarlo

   pinMode( 13, OUTPUT );

   Serial.begin( 115200 );

   Serial.println( "\n*** RESET ***" );

   vTaskStartScheduler();
}

void loop() {}

NOTA: El mecanismo anterior funciona de maravilla cuando son tareas las que están involucradas con el recurso; pero si se tratase de una interrupción quien fuera el productor o consumidor, entonces esta solución (hacer static al monitor) no funciona, ya que no existe modo de pasárselo a la interrupción. En tal caso el monitor debe ser global. Sin embargo, como verás a continuación, FreeRTOS recomienda no usar mutex’es dentro de interrupciones.

Otras funciones

Mutex’es e interrupciones

A pesar de que la API primitiva de semáforos incluye funciones espejo para ser utilizadas desde dentro de interrupciones (ISR’s) FreeRTOS recomienda no utilizar mutex’es dentro de ISR’s. Esto debido a dos cosas: la posible elevación temporal de la prioridad de la tarea que posee al mutex sólo tiene sentido a nivel de tarea, y una ISR no debería bloquearse esperando obtener al mutex. Por esta razón no mencionaré dichas funciones espejo.

Verificando si el mutex está disponible o no

En los ejemplos vistos hemos tratado de obtener directamente al mutex, y de no estar disponible, entonces la tarea se va al estado Blocked. Sin embargo, en algunas situaciones primero querremos saber si está disponible, y en caso negativo continuar sin tomarlo evitando que la tarea se bloquée. Para preguntar si el mutex está disponible puedes utilizar la función uxSemaphoreGetCount():

xSemaphore. Handler del mutex.

Valor devuelto. 1 si el mutex está disponible, y 0 si no lo está.

Preguntando cuál tarea posee al mutex

La función uxSemaphoreGetCount() te dice si el mutex está disponible o no, pero no te dice quién lo tiene; la función xSemaphoreGetMutexHolder() te indica cuál tarea lo posee. El valor devuelto por esta función está un poco enredado, así que intentaré explicarlo lo más simple posible:

xMutex. Handler del mutex del cual estamos preguntando.

Valor devuelto. Esta función puede devolver dos valores:
1. El handler de la tarea que lo posee, si es que alguna tarea lo posee, es decir, no está disponible.
2. NULL en cualquiera de los siguientes dos casos:
2.1 El semáforo indicado en xMutex no es un semáforo mutex, o
2.2 El mutex está disponible, y por lo tanto, ninguna tarea lo posee.

Suponiendo que no has mezclado diferentes tipos de semáforos, entonces bastaría con asumir que el valor NULL significa que ninguna tarea lo posee.

¿Qué sigue?

Hoy mencionamos los diversos problemas que enfrentamos cuando programamos de manera concurrente, y el problema que atacamos fue el de las condiciones de carrera, es decir del acceso por dos o más tareas a un mismo recurso compartido al mismo tiempo. Vimos que una solución es tratar al recurso como una sección crítica y limitar el acceso esta sección con un semáforo de exclusión mutua, o mutex.

Así mismo, llevamos el concepto de mutex un paso adelante con los monitores. Un monitor encapsula al mutex y al recurso compartido, y solamente permite que el cliente accese a éste a través de un conjunto mínimo de operaciones, evitando que se utilice mal o sobre un recurso diferente.

Índice del curso

Espero que esta entrada haya sido de tu interés. Si fue así podrías suscribirte a mi blog, o escríbeme a fjrg76 dot hotmail dot com, o comparte esta entrada con alguien que consideres que puede serle de ayuda.


Fco. Javier Rodríguez
Escrito por:

Fco. Javier Rodríguez

Soy Ingeniero Electrónico con 20+ años de experiencia en el diseño y desarrollo de productos electrónicos de consumo y a medida, y 12+ años como profesor. Egresado de la UNAM, también tengo el grado de Maestro en Ingeniería por la misma universidad. Mi perfil completo lo puede encontrar en: https://www.linkedin.com/in/fjrg76-dot-com/

Ver todas las entradas

5 COMENTARIOS