Variables de condición y monitores, I

Esta entrada la quiero presentar a través de dos ejemplos y algunas preguntas.

Imagina un sistema concurrente o multitarea con sistema operativo, como muchos de los que hemos estado escribiendo. Imagina que tienes una estructura de datos Cola (Queue en inglés) y que es compartida por varias tareas, es decir, dos o más tareas tratarán de escribir o leer de ella. Los problemas que se presentan son:

  1. ¿Qué haces si la cola está vacía y una o más tareas necesitan leer de ella?
  2. ¿Qué haces si la cola está llena y una o más tareas necesitan escribir en ella?
  3. ¿Qué haces si mientras una tarea está escribiendo en la cola otra quiere leer de ella «al mismo tiempo»?

Este otro ejemplo es clásico en el tema que vamos a tratar hoy. Imagina un sistema bancario que puede recibir depósitos y puede entregar dinero en efectivo. Varias personas pueden depositar, mientras que una o más pueden retirar.

  1. ¿Qué haces si dos personas quieren depositar al mismo tiempo?
  2. ¿Qué haces si dos personas quieren retirar al mismo tiempo?
  3. ¿Qué haces si mientras una persona está depositando otra quiere retirar, o viceversa?
  4. ¿Qué haces si quieres retirar una cantidad, pero el saldo no es suficiente?

El punto en común de ambos ejemplos es que hay un recurso compartido que por un lado de ninguna manera debe ser accesado al mismo tiempo, so pena de corromperlo, y por otro lado tiene tiempos de inactividad en que el solicitante debe esperar (no hay datos para desencolar, la cola está llena, saldo insuficiente para cubrir el retiro, etc).

Una solución es que si el recurso está siendo utilizado, los solicitantes se agreguen a una lista de espera y se vayan a dormir. En cuanto el recurso es liberado, uno de los solicitantes en la lista de espera es sacado de la misma y despertado para que use al recurso. A esta solución le vamos a llamar Variable de condición (en inglés, Condition variable).

Y por otro lado tenemos a los monitores. Un monitor es una construcción (en forma de tipo abstracto o clase) que encapsula a un recurso compartido, administra el acceso a éste a través de un semáforo mutex, y presenta un conjunto de operaciones al cliente del mismo. El recurso sólo puede ser utilizado a través de las operaciones públicas, es decir, el recurso no puede ser usado de maneras que no hayan sido previstas; además, es el monitor el que se encarga de la administración del mutex, no el usuario del recurso. Un monitor puede usar o no variables de condición.

En esta primera parte del artículo de hoy voy a mostrarte una implementación de variable de condición para un microcontrolador con núcleo ARM Cortex-M3 y el sistema operativo FreeRTOS. Por supuesto que las ideas expuestas, así como el código, los podrás trasladar a otras arquitecturas y sistemas operativos con un mínimo de esfuerzo. En la segunda parte te mostraré cómo implementar un monitores con y sin variables de condición.

Tabla de contenidos

¿Qué es y qué necesito?

Un semáforo mutex (de aquí en adelante solamente ‘mutex’) puede implementar exclusión mutua simple; esto es, nada más una tarea puede accesar a un recurso compartido (o estar activa dentro de una región crítica) en un tiempo dado. Para que otra tarea accese al recurso, la tarea que tenía el mutex lo debe devolver. Esta situación hace imposible que la tarea que tiene al mutex, y por lo tanto al recurso compartido, sea suspendida y despertarla después cuando una condición se cumpla. (En mi curso de Arduino en tiempo real escribí esta entrada sobre los mutex’es.)

En los ejemplos que dí al principio lo podemos observar: una tarea que quiere escribir a la cola ya tiene el mutex, es decir, ya tiene acceso a la cola, pero ésta está llena, y por lo tanto no se puede escribir en ella, ¿qué debe hacer la tarea que tiene el acceso a la cola y quiere escribir? Puede hacer dos cosas:

  1. Devolver el mutex y salir para después tentar a la suerte e intentar adquirir al mutex (y al recurso) cuando se haya hecho algo de espacio, o
  2. Ser insertada en una cola de espera, irse a dormir, y después ser desencolada de manera automática tan pronto se haya hecho algo de espacio en la cola.

El punto 2 es la definición de una variable de condición: una tarea a la espera del mutex es encolada y suspendida, y cuando la condición (en este ejemplo, hay espacio en la cola) se cumple, la tarea es desencolada, despertada, adquiere el mutex y se pone a trabajar. La cola de espera permite que varias tareas estén suspendidas (dormidas) a la espera del mutex, y cada una lo obtendrá tan pronto la condición se cumpla.

Una variable de condición es un tipo abstracto que presenta, al menos, las siguientes operaciones:

  • CWait(). Si el mutex no está disponible, entonces la tarea que hizo la llamada a esta función se encola y se suspende.
  • CNotify(). Avisa que la condición se cumplió, y por lo tanto, la tarea que lleva más tiempo en la cola de espera es despertada y puesta a trabajar. Si no hubiera ninguna tarea esperando, entonces no se hace nada.

(La letra C en los nombres anteriores es de Condition, pero es lo de menos.)

En su forma más básica los atributos de este tipo abstracto es una estructura de datos Cola, es decir, el primer elemento en haber entrado (el más antiguo) será el primero en salir (en inglés, FIFO: First-in, First-out). Aunque el mutex puede ser parte del mismo, la mayoría de implementaciones que he visto lo manejan de forma externa, es decir, son parte de la lista de argumentos de la función CWait(). Quizás más adelante modifique (o mejor aún, tú modifiques) la clase CondVar para que también provea al mutex.

Así mismo, además de las dos funciones básicas mencionadas, también se podrían incluir las siguientes tres:

  • CWaitFor(). Igual que CWait(), pero si no obtiene al mutex antes de que el tiempo (relativo a cuando se hizo la llamada) establecido expire sale y devuelve el valor false.
  • CWaitUntil(). Igual que CWait(), pero si no obtiene al mutex antes de que el tiempo (absoluto con referencia a una marca de tiempo en el pasado) establecido expire sale y devuelve el valor false.
  • CNotifyAll(). Igual que CNotify(), pero despierta a todas las tareas que estuvieran en la cola de espera.

El ejemplo que te voy a mostrar más adelante incluye las funciones CWait() y CNotify(). Y hablando de ejemplos, antes de ver los pseudo códigos correspondientes creí necesario que supieras que tanto las variables de condición, como los monitores (que veremos en la segunda parte), pueden ser codificados de manera procedimental (en lenguaje C), u orientada a objetos (con clases en lenguaje C++, Java, C#, etc). El ejemplo te lo voy a mostrar en C++ con objetos, pero verás que no tendrás ningún problema es hacerlo en C plano.

Operación CWait

El núcleo de una variable de condición es la operación CWait(), cuyo pseudo código es:

CWait( mutex )
{
    Inserta a la tarea en la cola de espera
    Devuelve el mutex
    La tarea se suspende

    // esta es la instrucción que la tarea ve cuando es despertada:
    Adquiere al mutex
}

Ojalá su implementación fuera tan simple como su pseudo código. Nota que cuando la tarea es despertada lo primero que hace es intentar adquirir al mutex de nuevo (en la línea 4 lo devolvió).

Ahora, entre el momento de que la tarea fue despertada y el momento en que obtuvo al mutex de regreso, muchas cosas pudieron haber sucedido, entre ellas que la condición dejara de cumplirse. Sí, podría pasar. Por ejemplo, si la tarea estaba esperando un espacio para escribir, pero antes de obtener nuevamente el mutex otra tarea llenó la cola, entonces debe volver a esperar. Esta condición se puede manejar así:

Mutex mutex

una_tarea()
{
    Mientras 1 == 1
    {
        Obtiene al mutex

        Mientras la condición no se cumpla
        {
            CWait( mutex )
        }

        // usa al recurso 

        Devuelve al mutex
    }
}

¿Y cómo sabe que la condición se cumplió? ¡Ah! Aquí es donde la función CNotify() entra a escena.

Operación CNotify()

Cuando el código del cliente de la variable de condición detecta que la condición se ha cumplido, entonces manda llamar a CNotify(), cuyo pseudo código es:

CNotify()
{
    Si hay elementos en la cola de espera
    {
        Desencola a la tarea que ha esperado por más tiempo
        La despierta
    }
}

Que una tarea sea despertada no significa que vaya a ser ejecutada inmediatamente; el sistema operativo la pone como candidata a ser ejecutada (es decir, en la lista de tareas Ready) y se ejecutará cuando le corresponda.

¿Cómo se usan ambas funciones, CWait() y CNotify()? Veámoslo en un pseudo código:

una_tarea()
{
    Mientras 1 == 1
    {
        Obtiene al mutex

        Mientras la condición no se cumpla 
        {
            CWait( mutex )
        }

        // usa al recurso 

        Devuelve al mutex
    }
}
    
otra_tarea()
{
    Mientras 1 == 1
    {
        Si la condición se ha cumplido 
        {
            CNotify()
        }
    }
}

La función otra_tarea está revisando el cumplimiento de la condición, y cuando ésta ha sido cumplida, manda llamar a CNotify(), la cual despierta, de existir, a la tarea que más tiempo ha estado esperando. Como la condición ya fue cumplida, el ciclo wait deja de ejecutarse y la tara una_tarea puede accesar al recurso.

Desarrollo

Una vez que ya tenemos una idea de qué son y cómo se usan las variables de condición, es momento de aterrizar los conceptos. Los libros e Internet están llenos de teoría, y me costó mucho trabajo encontrar ejemplos concretos. Por eso, además de realizar la implementación de este tipo de mecanismos, es que quise compartirla contigo por si en algún momento tienes que usarlas.

Función CWait() y compañía

La función CWait() en C/C++, y utilizando al sistema operativo FreeRTOS, queda de la manera siguiente. Como mencioné, si estás utilizando C plano o algún otro sistema operativo me parece que no tendrás problemas en adaptarlo. Al final del artículo te pasaré el repositorio donde podrás descargar el código completo para el microcontrolador LPC1549 de 32 bits, de núcleo Cortex-M3.

   bool CWait( SemaphoreHandle_t& mutex, TickType_t ticks = portMAX_DELAY )
   {
      TaskHandle_t self;

      self = xTaskGetCurrentTaskHandle();
      // obtiene la tarea actual

      taskENTER_CRITICAL();
      {
         // guarda la tarea en la cola de espera:
         configASSERT( this->len < this->max );
         this->queue[ this->tail++ ] = self;
         if( this->tail == this->max ) this->tail = 0;
         ++this->len;

      } taskEXIT_CRITICAL();

      xSemaphoreGive( mutex );
      // devuelve el mutex

      vTaskSuspend( self );
      // la tarea se auto suspende
     
      return xSemaphoreTake( mutex, ticks );
      // intenta reobtener al mutex
   }

Nota que el código que tiene que ver con guardar la tarea (en realidad, su handler o descriptor) en la cola de espera está dentro de una sección crítica. No queremos que sucedan los problemas que mencioné en un principio, es decir, que mientras una tarea está intentando insertarse en la cola, otra, quizás de mayor prioridad, le gane. Usé una sección crítica para que la operación de inserción (y más adelante, la de extracción) fuese atómica, pero también es posible utilizar un mutex dedicado a la variable de condición con el mismo propósito. De hecho muchos libros lo hacen así (en la teoría). En un futuro cambiaré la sección crítica por un mutex dedicado.

CWait() espera de manera indefinida para obtener el mutex. Su argumento ticks indica que espere por siempre (gracias al valor por defecto portMAX_DELAY); esto es como si el argumento no existiera, y así debe ser para esta función. Sin embargo, las funciones CWaitFor() y CWaitUntil() sí que lo usan:

   bool CWaitFor( SemaphoreHandle_t& mutex, TickType_t ticks )
   {
      return CWait( mutex, ticks );
   }

   bool CWaitUntil( SemaphoreHandle_t mutex, TickType_t& last, TickType_t ticks ) 
   {
      TickType_t now = xTaskGetTickCount();

      TickType_t until = last + ticks - now;
      // los overflows se manejan de manera automática

      last = until;

      return CWait( mutex, until );
   }

Si estás familiarizado con la función de FreeRTOS vTaskDelayUntil(), no tendras problema para entender a la función CWaitUntil() que funciona de manera similar: el tiempo establecido es absoluto, es decir, está referido a un punto establecido en el pasado, el cual se actualiza antes de salir de la función. Con esto evitas el desplazamiento de las llamadas en la línea de tiempo. Sin embargo, si tu aplicación no tiene problemas con ello, entonces puedes usar CWaitFor(), la cual establece un tiempo de salida relativo a cuando fue llamada, similar a la función de FreeRTOS vTaskDelay(). En esta lección de mi curso gratuito Arduino en tiempo real explico las diferencias y los usos de ambas funciones, vTaskDelay() y vTaskDelayUntil().

Función CNotify() y compañía

La función CNotify() en C/C++, y utilizando al sistema operativo FreeRTOS, queda de la manera siguiente. Es más fácil que su contraparte CWait(), pero igual de importante, ya que es ésta la que vuelve a la vida a las tareas para que continuen con su trabajo:

   void CNotify()
   {
      TaskHandle_t task = nullptr;

      taskENTER_CRITICAL();
      {
         if( this->len > 0 ){
            task = this->queue[ this->head++ ];
            if( this->head == this->max ) this->head = 0;
            --this->len;
         }
      } taskEXIT_CRITICAL();

      if( task != nullptr ) vTaskResume( task );
   }

RECUERDA: Hablando de la misma variable de condición, una tarea llama a CWait(), mientras que otra debe llamar a CNotify(). En otras palabras, las tareas consumidoras llaman a CWait(), mientras que las tareas productoras llaman a CNotify().

La sección crítica evita que si dos tareas llaman a CNotify() al mismo tiempo interfieran entre sí y corrompan a la cola de espera.

CNotify() despierta a la tarea que ha esperado por más tiempo, de haberla. La función CNotifyAll() despierta a todas:

   void CNotifyAll()
   {
      taskENTER_CRITICAL();
      {
         while( this->len ) CNotify();
      } taskEXIT_CRITICAL();
   }

Ejemplo

Ahora que conocemos qué son, qué hacen y cómo se implementan las variables de condición, ya podemos ver un ejemplo simple. El recurso compartido será una cola de caracteres, y habrá una función put() que escriba en ella y una función get() que lea de ella. Para poder escribir es necesario que haya espacio, y para poder leer es necesario que hayan caracteres disponibles. Para solucionar este problema vamos a usar dos variables de condición, una por cada condición (¡Wow!)

Primero declaramos las dos variables de condición:

ConditionV<4> data_avail;
// Variable de condición que indica que hay caracteres disponibles para leer

ConditionV<4> space_avail;
// Variable de condición que indica que hay espacio disponible en la cola de caracteres

El valor 4 indica el máximo número de tareas que la cola de espera de la variable de condición podrá guardar. En tu aplicación tú decides si son más o son menos.

La función put() queda así:

void put( char c )
{
   xSemaphoreTake( mutex, portMAX_DELAY );
   // adquiere al recurso

   while( len >= max ) space_avail.CWait( mutex );
   // la tarea se bloquea si la cola de caracteres está llena
   // la función get() realiza la notificación

   // escribe un carácter en la cola:
   buf[ tail++ ] = c;
   if( tail >= max ) tail = 0;
   ++len;

   data_avail.CNotify();
   // avisa que hay al menos un carácter listo para ser leído,
   // la usa la función get()

   xSemaphoreGive( mutex );
   // devuelve al recurso
}

La función get() es:

char get()
{
   xSemaphoreTake( mutex, portMAX_DELAY );
   // adquiere al recurso

   while( len == 0 ) data_avail.CWait( mutex );
   // la tarea se bloquea si la cola de caracteres está vacía
   // la función put() realiza la notificación

   // lee un carácter de la cola:
   char c = buf[ head++ ];
   if( head >= max ) head = 0;
   --len;

   space_avail.CNotify();
   // avisa que hay al menos un espacio para escribir,
   // la usa la función put()

   xSemaphoreGive( mutex );
   // devuelve al recurso

   return c;
   // devuelve al carácter
}

Las tareas productoras y consumidoras son las siguientes (en realidad es una de cada una, pero las reutilicé) :

void producer( void* pvParameters )
{
   uint32_t offset = (uint32_t)pvParameters;
   char cont = 0;

   char* task_name = pcTaskGetName( NULL );

   char letter = offset;

   while( 1 )
   {
      put( letter++ );
      // intenta escribir un carácter en la cola

      ++cont;
      if( cont == 10 ){
         cont = 0;
         letter = offset;
      }

      vTaskDelay( pdMS_TO_TICKS( ( rand() % 50 ) + 20 ) );
   }
}

void consumer( void* pvParameters )
{
   char* task_name = pcTaskGetName( NULL );

   while( 1 )
   {
      char c = get();
      // intenta leer un carácter de la cola

      vTaskDelay( pdMS_TO_TICKS( ( rand() % 150 ) + 25 ) );
   }
}

Quizás notaste que los tiempos que ambas tareas establecen para irse dormir (y así liberar la CPU) son aleatorios. Esto no es requisito para las variables de condición. Lo hice así con el fin de probar el código en condiciones cercanas a la realidad.

Para completar el ejemplo veamos la creación de las tareas:

   xTaskCreate( producer, "P0", 128,  (void*) 'A', tskIDLE_PRIORITY + 1, NULL ); // prints out: ABC...J
   xTaskCreate( producer, "P1", 128,  (void*) 'a', tskIDLE_PRIORITY + 1, NULL ); // prints out: abc...j
   xTaskCreate( producer, "P2", 128,  (void*) '0', tskIDLE_PRIORITY + 1, NULL ); // prints out: 012...9

   xTaskCreate( consumer, "C0", 128, NULL, tskIDLE_PRIORITY, NULL );
   xTaskCreate( consumer, "C1", 128, NULL, tskIDLE_PRIORITY, NULL )

Código fuente

El código fuente completo de este artículo lo puedes descargar o clonar desde este commit del repositorio del proyecto; pero si quieres obtener la última versión completa, incluyendo al monitor (tema de la segunda parte), lo puedes hacer desde aquí.

¿Qué sigue?

Hoy vimos qué son y cómo se implementan las variables de condición, herramientas muy útiles en la programación multi tarea.

Utilicé secciones críticas en las funciones CWait() y CNotify(). Una sección crítica detiene a todo el sistema porque desactiva las interrupciones. Una mejora, como lo mencioné, sería agregar un mutex dedicado a la variable de condición en lugar de las secciones críticas. Con esto logramos que el sistema siga funcionando independiente de las variables de condición.

Otra modificación muy interesante de cara a los sistemas de tiempo real es cambiar la cola de espera, que es circular, por una cola de espera doble, o una cola de prioridad, de tal manera que las tareas con prioridad alta sean las primeras en ser despachadas. No es fácil tomar la decisión ya que las tareas de alta prioridad podrían quedarse con la CPU y nunca prestársela a las tareas con baja prioridad que estuvieran esperando por el recurso. De cualquier manera vale la pena realizar el análisis y la modificación de la misma. O en su defecto, tener dos versiones de variables de condición: modo «normal» y modo «tiempo real».

En la segunda parte abordaré el tema de los monitores, los cuales, como también mencioné, pueden o no usar variables de condición.


Si encuentras este blog interesante, entonces podrías considerar suscribirte a él y recibir información relevante sobre tecnología y sistemas embebidos, y de vez en cuando, uno que otro regalo.

Semáforos Mutex

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.


Si encuentras este blog interesante, entonces podrías considerar suscribirte a él y recibir información relevante sobre tecnología y sistemas embebidos, y de vez en cuando, uno que otro regalo.