Como utilizar Semáforos no mbed OS - Sincronização

Threads no MBED OS Semáforos no mbed OS Recurso compartilhado
Este post faz parte da série mbed OS. Leia também os outros posts da série:

Olá caro leitor! Neste artigo iremos seguir a exploração dos recursos presentes no mbed OS da ARM. No artigo anterior exploramos as Threads, e chegamos a conclusão que as Threads possuem um único objetivo, executar um algoritmo ou aplicação até que isso não seja mais desejado pelo sistema. Os semáforos, são um pouco mais amplos no quesito de objetivos de uso, por isso dividirei o assunto em duas partes. Neste artigo começaremos falando sobre o uso de Semáforos no mbed OS para uso em sincronização de Threads.

 

  

O que é um Semáforo?

 

O Semáforo de um modo geral é um objeto de kernel (recurso que faz parte do núcleo do sistema operacional podendo atuar sob a forma como ele deve executar a Thread corrente ou as demais) que consiste em um contador e limite de contagem. Sua política de uso consiste em decrementar (utilizar o Semáforo) esse contador e incrementar (liberar o Semáforo para uso). O ato de utilizar ou liberar um Semáforo possui consequências que se refletem na execução das Threads do sistema operacional de forma que:

  • A liberação do Semáforo pode ser feita sem restrição, ou seja, toda e qualquer tarefa pode incrementar o contador de uso, de modo que caso o contador ultrapasse o limite ele sofrerá uma operação de saturação (nunca passará do limite configurado);

 

  • A utilização do Semáforo é condicionada à disponibilidade, ou seja, toda Thread que tentar decrementar o contador, está sujeita a aguardar até que o contador possua valor para decremento. Caso esse seja 0, a Thread entra em uma lista e aguarda que o contador possua pelo menos uma unidade acima de 0.

 

Em um kernel com características preemptivas como o mbed OS, a liberação de um Semáforo, mesmo não possuindo qualquer restrição, pode resultar na troca de Threads em execução. Isso ocorre porque ao incrementar o contador do Semáforo, se alguma Thread de maior prioridade que a corrente estiver aguardando por esse mesmo Semáforo, ela imediatamente irá para a lista de tarefas prontas, decrementando o Semáforo novamente, e sendo colocada em execução, ao passo que a Thread corrente, volta para o estado de Pronta, aguardando que a CPU seja liberada.

 

 

Semáforo: Agente Sincronizador de Tarefas

 

Agora que sabemos o fundamento de operação do Semáforo, poderemos explorar seu uso como agente sincronizador de tarefas. Para isso, vamos considerar o seguinte caso com os seguintes participantes:

  • Thread A, prioridade Alta;
  • Thread B, prioridade Baixa;
  • Semáforo S, limite de contagem unitário e contagem inicial igual a zero.

 

Nosso kernel, nesse momento, tem apenas as Threads A e B instaladas e S instanciado. Suponhamos então que a thread A precise que um dado processamento dependente da thread B ocorra para que ela possa atingir seu objetivo. Porém A tem prioridade maior que B e com isso B que precisa executar um processamento que libera A para rodar corretamente, nunca ocorre, levando A a nunca atingir seu objetivo, e B eternamente ficará desligado. Como resolver essa situação? Temos duas possibilidades, vamos averiguar:

  • Aumentar a prioridade de B sobre A? Resolve parte do problema, pois se A precisa rodar sempre que estiver pronta, elevando a prioridade pode fazer com que A não rode sempre, isso se rodar;
  • Manter A e B com mesma prioridade? Nesse caso estaríamos forçando o kernel a rodar no modo cooperativo, de forma que a primeira tarefa que entrar na lista de prontas irá executar por tempo indeterminado. O que também não resolve o problema.

 

A terceira opção, pode ser um caminho para contornar o problema, a utilização do Semáforo S. Vamos analisar: A possui maior prioridade e pode executar até o ponto que necessite do processamento realizado por B, então A utiliza (decrementa)  S, porém S está com contagem igual a 0, fazendo com que A seja imediatamente suspensa, dando lugar a B que mesmo com menor prioridade fica apta a rodar. B agora está em execução e pode realizar o processamento que A precisa.

 

Vejam que interessante, B não sabe nem da existência de A, mas o sistema consegue estimar exatamente onde A parou de executar e até onde B tem que executar. Então B executa esse processamento, porém outras partes de seu algoritmo são indesejáveis para execução nesse momento. Então B acessa S e libera um semáforo (incrementa) e graças à preempção do kernel, A imediatamente toma conta da CPU, suspendendo B e termina sua execução, e quando acabar, ela libera a CPU para que B também termine sua execução.

 

Observem que no exemplo acima, uma Thread não conhece a outra, mas com o uso do semáforo podemos parar uma dada Thread num ponto especifico, esperar que outra Thread chegue em um ponto desejado e em seguida dar prosseguimento à sua execução. Isso é o efeito da sincronização. A figura abaixo mostra graficamente um esboço do que aconteceu na prática:

 

Semáforos no mbed OS - Operação do Semáforo
Figura 1: Operação do Semáforo

 

 

Como faço para implementar Semáforos no MBED-OS?

 

O MBED-OS oferece recursos para semáforos dos mais variados tipos. O semáforo de contagem (um dos favoritos para sincronizar Threads) é a opção padrão, bastando apenas instanciar a classe Semaphore, sem passar qualquer parâmetro de inicialização. Para testar o exemplo de sincronização, o leitor deverá seguir os passos citados no artigo sobre Threads para criar um projeto com o mbed OS. Após seguir os passos, no arquivo main.cpp, copie e cole o seguinte código:

 

 

O código acima demonstra o uso da sincronização entre três Threads, de prioridades diferentes. Compile o código e gere o binário para download pressionando CTRL + D. Em seguida, conecte a placa LPC4337 no seu computador e arraste o arquivo gerado, espere a placa ser re-enumerada na USB, pressione o botão de reset, você deve ver algo parecido com o vídeo abaixo:

 

 

Vejam que legal, mesmo sendo três Threads diferentes e com prioridades diferentes, todas elas rodam,  e rodam em sequência, pois os semáforos são utilizados e liberados em pontos estratégicos das Threads fazendo com o que a aplicação execute o fluxo determinado pelo desenvolvedor, e não apenas o desejado pela política de agendamento do kernel.

 

 

Explicando o código

 

Bom, agora que já vimos a demonstração, vamos ver o que esse novo código tem de diferente. Primeiramente explicarei como instanciar os semáforos:

 

Vejam que foram criados apenas dois semáforos, um terceiro não se faz necessário, pois a própria característica preemptiva do kernel se encarrega de suspender a tarefa de mais baixa prioridade na liberação do semáforo (igual ao caso de uso explicado acima), e por falar em prioridades, vejamos:

 

Sim, são exatamente as mesmas Threads criadas no artigo anterior em que falamos sobre o assunto, perfeito para ilustrar o exemplo de sincronização. Vamos ver o que temos na função  principal main:

 

Nada mudou, como podemos ver, o display se configura, acerta o contraste, uma mensagem de boas vindas será impressa na tela, e em seguida as três Threads criadas, terão suas execuções iniciadas. Sabemos então que a tarefa rt_task() será a primeira a rodar:

 

No primeiro momento, a Thread de mais alta prioridade irá iniciar sua execução, e quando chegar num ponto específico da função ela irá tentar utilizar o semáforo rt_sema, porém seu contador está zerado, com isso rt_task ficará suspensa dando lugar para hp_task() rodar. Essa nova Thread é exatamente igual, só que sua prioridade é maior que osPriorityNormal e menor que osPriorityRealtime, então ela vai executar até um ponto especificado, onde tentará utilizar o semáforo hp_sema, e da mesma forma que a Thread anterior, ela irá suspender. Com isso a única Thread disponível, será a np_task(): 

 

A np_task irá iniciar a execução, realizando algum processamento, e da mesma forma que no nosso estudo de caso, quando o ponto que ela executou for relevante para rt_task, ela irá liberar o semáforo. Como rt_task possui maior prioridade que todas as Threads, ela será posta em execução. O loop for inserido representa um processamento qualquer e ajuda na visualização das transições do estado do semáforo. A execução procede e em dado momento a Thread novamente usa a variável rt_sema, que está zerado novamente.

 

Com isso np_task volta a rodar e executa até um ponto que se torna relevante para hp_task. Com isso np_task libera dessa vez o semáforo hp_sema. Imediatamente hp_task irá executar, pois sua prioridade é maior que a de np_task, e rt_task encontra-se suspensa pois aguarda que alguém libere seu semáforo. hp_task executa mais algumas instruções e pega novamente hp_sema e suspende. Com isso np_task (que nunca aguarda semáforo) roda novamente reiniciando o ciclo. Assim temos uma Thread capaz de executar outras Threads de forma alternada.

 

 

Conclusão

 

Semáforos são um pouco mais amplos, então o propósito desse artigo foi demonstrar o uso desse recurso no mbed OS em forma de serviço de sincronização para organizar o fluxo de execução das Threads com o gosto do desenvolvedor. No exemplo explicado nesse texto demonstramos como sincronizar três Threads de forma que rodem alternadamente, sem o uso do popular delay, e de forma que conseguimos controlar totalmente o fluxo de execução de cada uma das tarefas da nossa aplicação. E voce leitor? O que achou do uso do semáforo? Bons projetos!

 

 

Links úteis sobre Semáforos no mbed OS

 

Referência de uso da classe Semaphore do mbed OS, clique aqui .

Meu repositório no mbed contendo o projeto exemplo completo, clique aqui.

Outros artigos da série

<< Como utilizar as Threads no mbed OSComo utilizar os semáforos para compartilhar recursos no mbed OS >>
Este post faz da série mbed OS. Leia também os outros posts da série:
NEWSLETTER

Receba os melhores conteúdos sobre sistemas eletrônicos embarcados, dicas, tutoriais e promoções.

Obrigado! Sua inscrição foi um sucesso.

Ops, algo deu errado. Por favor tente novamente.

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.

Felipe Neves
Engenheiro de sistemas embarcados apaixonado pelo que faz, já trabalhou em diversos setores de tecnologia nos últimos 14 anos com destaque para Defesa, Automação, Agricultura, Wearables, Robótica e mais recentemente Semicondutores. Possui sangue maker, tendo paixão por construir e compatilhar suas próprias coisas e explorar novos sabores dentro do mundo "embedded". Atualmente trabalha como Engenheiro de Software Senior na Espressif, sim aquela do ESP32 e do ESP8266. Tem interesse em tópicos que envolvam Robótica, Controle de Movimento, DSP e Sistemas de Tempo Real.

Deixe um comentário

avatar
 
  Notificações  
Notificar