Synergy e ThreadX: recursos compartilhados e Mutex

Nos artigos anteriores vimos como criar threads no Synergy/ThreadX e conhecemos um pouco sobre a estrutura básica do RTOS. Agora chegou a hora de conversar sobre um erro muito comum quando se programa num paradigma de multiprocessamento que é o de não proteger recursos compartilhados, resultando potencialmente em race conditions (condições de corrida). Mas o que é exatamente um recurso compartilhado? Qualquer periférico que seja utilizado por mais de uma thread é um recurso compartilhado! Se duas ou mais threads escrevem em pinos diferentes de uma mesma porta de E/S, essa porta é um recurso compartilhado, se duas ou mais threads escrevem dados em um display, o display é um recurso compartilhado e assim por diante. Se a operação em questão não for atômica, ou seja, se ela não puder ser executada em uma única instrução, então a operação estará sujeita a falhas caso o recurso seja compartilhado entre duas ou mais threads.

Vamos tomar como exemplo um display LCD: suponha que duas threads (A e B) efetuem operações no display, cada uma escrevendo dados numa área do mesmo (A escreve na linha 0 e B na linha 1). Agora suponha que  A está escrevendo na linha 0 a frase “Voltage = 5.02” mas sofre preempção quando escreveu apenas “Vol”. Então a thread B é resumida e inicia a escrita da frase “Current = 1.08” na linha 1 do display, mas sofre preempção quando já escreveu “Current =” e o kernel retoma a thread A do ponto em que parou. O que irá acontecer? Infelizmente a resposta é que nem a thread A nem o recurso compartilhado (o display) tem como saber que existem duas operações ocorrendo em paralelo e o que ocorre é que a thread A continua escrevendo no display, mas a partir do ponto onde ele (o display) estava, ou seja, “Current =” e o resultado é que teremos na segunda linha algo como “Current =age = 5.02”!

Este exemplo embora tolo permite demonstrar a importância com que se deve abordar recursos compartilhados. Algumas pessoas poderão argumentar que a chance de isso ocorrer é muito pequena, mas é importante perceber que a situação existe e pode ser potencialmente catastrófica! O nosso exemplo utilizou um display, que permite identificar facilmente uma falha oriunda do acesso concorrente a  um recurso compartilhado, mas em alguns casos, falhas decorrentes de race conditions podem ficar latentes por dias, meses ou anos! E acredite, segundo a lei de Murphy, estas falhas provavelmente aparecerão durante uma apresentação ao seu chefe, cliente ou em campo com o produto em uso!

A seguir apresentamos um exemplo que permite demonstrar e entender melhor como uma falha deste tipo pode se manifestar. O exemplo utiliza duas threads que escrevem individualmente no display LCD gráfico da placa SK-S7G2. Foi desenvolvida uma biblioteca de funções primitivas para desenhar e escrever no display da placa. Apenas para fins demonstrativos, foi criada uma função especial para desenhar caracteres no display utilizando variáveis internas estáticas. Destaque-se que é um erro utilizar variáveis estáticas em aplicações multi-thread porém, neste caso, o seu uso tem exclusivamente o propósito de demonstrar os problemas decorrentes do acesso concorrente a recursos compartilhados!

O código das duas threads é apresentado a seguir, assim como o resultado da execução do código. Se você quiser testar o código, ele está disponível no repositório sob o nome MultiThreadGLCD1.

Figura 1 – Thread 0
Figura 2 – Thread 1
Figura 3 – Resultado da execução do programa

Observe a presença de artefatos (lixo) abaixo das linhas onde as threads escrevem seus dados (“O”, “.”, “X” ou ” ” conforme a thread). Este é o resultado de estar ocorrendo preempção durante a execução da função glcd_printChar_TEST! Devido a presença das variáveis estáticas dentro da função, o estado da mesma não é totalmente preservado na troca de contexto (pois variáveis estáticas não são salvas na pilha), fazendo com que ao retomar a execução da thread, o contexto de glcd_printChar_TEST não seja mais o mesmo! Uma situação idêntica ocorreria se o recurso compartilhado fosse um periférico do microcontrolador sendo acessado por múltiplas threads!

Como podemos evitar este tipo de falha? Bom, a resposta ideal é: nunca compartilhe recursos globais! No primeiro exemplo, ao invés de ter duas threads escrevendo no display, o programador poderia concentrar as operações com o display em uma thread apenas (A, por exemplo). Neste caso, a thread B poderia apenas enviar mensagens para A com o conteúdo a ser apresentado!

Mas há situações onde o uso de recursos compartilhados é imprescindível e nestes casos devemos utilizar um mecanismo que evite a corrupção dos dados do recurso compartilhado. A forma mais fácil seria proteger a operação no recurso compartilhado evitando que a tarefa sofra preempção durante a mesma, ou seja, desativando as interrupções, por exemplo, mas isso é uma heresia e não deve ser feito jamais num RTOS já que isso comprometeria o tempo de resposta das outras threads e do kernel! Mas como proteger essa sessão crítica de código?

A resposta definitiva para este tipo de situação é a utilização de Mutex, um objeto do RTOS que garante a exclusão mútua, ou seja, apenas uma thread pode possuir o Mutex num dado momento, excluindo as demais até que o Mutex seja liberado pela thread que o detém. Uma thread pode requisitar (pegar) um Mutex, desde que o mesmo esteja livre, através de uma chamada à função tx_mutex_get() e após utilizá-lo deve liberá-lo através da função tx_mutex_put().

Um exemplo com Mutex

O trecho de código a seguir demonstra como utilizar Mutex para proteger um recurso compartilhado:

tx_mutex_get(&mutex,wait_time);
// recurso compartilhado aqui
tx_mutex_put(&mutex);

Note que as chamadas sempre fazem referência a um ponteiro para o objeto Mutex desejado e, no caso da função get, é necessário também especificar um tempo de espera caso o Mutex esteja em uso por outra thread. Os valores possíveis para wait_time são:

  • TX_NO_WAIT (ou 0x0) – não aguarda pea liberação do Mutex e retorna imediatamente;
  • TX_WAIT_FOREVER (ou 0xFFFFFFFF) – aguarda indefinidamente (com a thread bloqueada) até que o Mutex esteja disponível;
  • valores entre 1 e 0xFFFFFFFE – tempo (em ticks do RTOS) pela qual a função deve aguardar a liberação do Mutex;

Mas como poderíamos utilizar Mutex para proteger a nossa função de escrita no display no exemplo anterior? Tudo o que é necessário é criar um objeto Mutex no ThreadX (utilizando o Synergy Configuration) e requisitar o Mutex através da função tx_mutex_get antes de utilizar o recurso compartilhado e liberar o Mutex após o seu uso (função tx_mutex_put).

Figura 4 – Criação de um Mutex no Synergy
Figura 5 – Propriedades do Mutex

A seguir apresentamos o código da thread 0 que demonstra como fazer uso do Mutex.

Figura 6 – Thread 0 com uso de Mutex

Lembre-se de que é necessário alterar também o código da thread 1, de forma que sempre que se utilizar alguma função relacionada ao recurso compartilhado (no caso o LCD), tal acesso seja protegido pelo Mutex!

Após a modificação do código das threads, a execução do programa irá gerar a tela mostrada a seguir. O código do projeto está disponível em MultiThreadGLCD2.

Figura 7 – Threads acessando o LCD com Mutex

Agora o problema com artefatos (lixo) abaixo das linhas onde as threads imprimem desapareceu! Isso porque graças ao Mutex, apenas uma das threads pode fazer uso do recurso compartilhado num dado instante, fazendo com que a outra thread que esteja tentando utilizar o mesmo seja bloqueada até que o Mutex seja liberado e o recurso esteja disponível!

Herança de Prioridade

A utilização de Mutexes em RTOS pode, muitas vezes, sofrer de problemas relacionados ao nível de prioridade das threads. Suponha um sistema com quatro threads (A, B, C e D, sendo A a de prioridade mais alta e D a mais baixa) e que A e D acessam um recurso compartilhado utilizando um Mutex para garantir o acesso exclusivo ao mesmo. Suponha ainda que num determinado momento, D pegou o Mutex e está fazendo uma operação demorada com o recurso compartilhado, mas instantes após D começar a trabalhar com o mesmo, o contexto é trocado e A é resumida e também precisa acessar o recurso compartilhado, ela tenta fazer um get do Mutex mas é suspensa pois o mesmo está em uso por D. O sistema continua a operação, executando somente B, C e D, mas como B e C possuem prioridade mais alta que D, elas terão preferência e D somente será executada quando B e C estiverem ociosas. O problema é que A, a thread de mais alta prioridade, vai permanecer bloqueada todo este tempo esperando por D (a thread menos prioritária) e muito provavelmente isso não será admissível para a aplicação!

Para resolver este tipo de situação o ThreadX inclui um mecanismo chamado de herança de prioridade (priority inheritance) que, quando utilizado, permite que uma thread de baixa prioridade que esteja em posse de um Mutex, assuma momentaneamente a prioridade de uma thread de mais alta prioridade que esteja tentando fazer uso do mesmo!

No exemplo acima, quando A tentar fazer uso do Mutex (criado com a opção de herança de prioridade ativada) em posse de D, ela irá elevar a prioridade de D ao seu nível, garantindo que D possa fazer uso do recurso e liberá-lo para que A o utilize! Note que no instante em que D fizer o put do Mutex, a sua prioridade retornará ao nível original e ela poderá ser interrompida por threads mais prioritárias que estejam prontas para executar!

Para utilizar o mecanismo de herança de prioridade no Synergy, basta selecionar a opção nas propriedades do Mutex desejado, conforme ilustra a figura 5.

No próximo artigo falaremos sobre comunicação entre tarefas no ThreadX, até lá!

Referências

Leave a Reply