Como utilizar Semáforos no MBED OS - Sincronização - Embarcados
Semáforo: Agente Sincronizador de Tarefas
Como faço para implementar Semáforos no MBED-OS?
Links úteis sobre Semáforos no mbed OS
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 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.
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:
Figura 1: Operação do Semáforo
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:
/** * @brief Simple Semaphore using the mbed os demo */ #include "mbed.h" #include "rtos.h" #include "ST7567.h" /* LCD and font parameters */ #define LCD_HEIGHT 64 #define LCD_WIDTH 128 #define FONT_HEIGHT 10 #define FONT_WIDTH 5 /* allocate statically stacks for the three threads */ unsigned char rt_stk[1024]; unsigned char hp_stk[1024]; unsigned char lp_stk[1024]; /* creates three tread objects with different priorities */ Thread real_time_thread(osPriorityRealtime, 1024, &rt_stk[0]); Thread high_prio_thread(osPriorityHigh, 1024, &hp_stk[0]); Thread low_prio_thread(osPriorityNormal, 1024, &lp_stk[0]); /* creates semaphores to explore the capabilites of synchronization */ Semaphore rt_sema; Semaphore hp_sema; const char rt_message[] = {"rt_task() sema:\0"}; const char hp_message[] = {"hp_task() sema:\0"}; const char lp_message[] = {"np_task() exec:\0"}; /* creates a instance of display */ ST7567 disp(D11, D13, D12, D9, D10); /** * @brief real time prio task function */ static void rt_task(void) { disp.locate(0, FONT_HEIGHT * 2); disp.printf(rt_message); rt_sema.wait(); for(;;) { /* display the semaphore status */ disp.locate((sizeof(lp_message) * FONT_WIDTH), FONT_HEIGHT * 4); disp.printf("WAIT!"); disp.locate((sizeof(rt_message) * FONT_WIDTH), FONT_HEIGHT * 2); disp.printf("TOOK!"); /* put a dumming process in order to consume CPU */ for(int i = 0; i < 0x3FFFFFF; i++) (void)0; rt_sema.wait(); } } /** * @brief high prio task function */ static void hp_task(void){ disp.locate(0, FONT_HEIGHT * 3); disp.printf(hp_message); hp_sema.wait(); for(;;) { /* dsiplay the semaphore status */ disp.locate((sizeof(lp_message) * FONT_WIDTH), FONT_HEIGHT * 4); disp.printf("WAIT!"); disp.locate((sizeof(hp_message) * FONT_WIDTH), FONT_HEIGHT * 3); disp.printf("TOOK!"); /* put a dumming process in order to consume CPU */ for(int i = 0; i < 0x3FFFFFF; i++) (void)0; hp_sema.wait(); } } /** * @brief normal prio task function */ static void np_task(void) { disp.locate(0, FONT_HEIGHT * 4); disp.printf(lp_message); disp.locate((sizeof(lp_message) * FONT_WIDTH), FONT_HEIGHT * 4); disp.printf("RUNN!"); disp.locate((sizeof(rt_message) * FONT_WIDTH), FONT_HEIGHT * 2); disp.printf("WAIT!"); disp.locate((sizeof(hp_message) * FONT_WIDTH), FONT_HEIGHT * 3); disp.printf("WAIT!"); for(;;) { rt_sema.release(); disp.locate((sizeof(rt_message) * FONT_WIDTH), FONT_HEIGHT * 2); disp.printf("WAIT!"); disp.locate((sizeof(lp_message) * FONT_WIDTH), FONT_HEIGHT * 4); disp.printf("RUNN!"); Thread::wait(1000); hp_sema.release(); disp.locate((sizeof(hp_message) * FONT_WIDTH), FONT_HEIGHT * 3); disp.printf("WAIT!"); disp.locate((sizeof(lp_message) * FONT_WIDTH), FONT_HEIGHT * 4); disp.printf("RUNN!"); Thread::wait(1000); } } /** * @brief main loop */ int main(void) { const char banner[] = {"Embarcados MBED-OS\0"}; /* configures the display */ disp.cls(); disp.set_contrast(0x35); disp.locate((LCD_WIDTH - (sizeof(banner) * FONT_WIDTH)) / 2, (LCD_HEIGHT/2) - (FONT_HEIGHT/2)); disp.printf(banner); Thread::wait(2500); disp.cls(); /* starts the three tasks */ real_time_thread.start(rt_task); high_prio_thread.start(hp_task); low_prio_thread.start(np_task); return 0; }
/* creates semaphores to explore the capabilites of synchronization */
Semaphore rt_sema;
Semaphore hp_sema;
const char rt_message[] = {"rt_task() sema:\0"};
const char hp_message[] = {"hp_task() sema:\0"};
const char lp_message[] = {"np_task() exec:\0"};
rt_sema.wait();
/* display the semaphore status */
disp.printf("WAIT!");
disp.printf("TOOK!");
/* put a dumming process in order to consume CPU */
for(int i = 0; i < 0x3FFFFFF; i++) (void)0;
hp_sema.wait();
/* dsiplay the semaphore status */
disp.printf("RUNN!");
rt_sema.release();
hp_sema.release();
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.
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:
/* creates semaphores to explore the capabilites of synchronization */ Semaphore rt_sema; Semaphore hp_sema;
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:
/* allocate statically stacks for the three threads */ unsigned char rt_stk[1024]; unsigned char hp_stk[1024]; unsigned char lp_stk[1024]; /* creates three tread objects with different priorities */ Thread real_time_thread(osPriorityRealtime, 1024, &rt_stk[0]); Thread high_prio_thread(osPriorityHigh, 1024, &hp_stk[0]); Thread low_prio_thread(osPriorityNormal, 1024, &lp_stk[0]);
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:
/** * @brief real time prio task function */ static void rt_task(void) { disp.locate(0, FONT_HEIGHT * 2); disp.printf(rt_message); rt_sema.wait(); for(;;) { /* display the semaphore status */ disp.locate((sizeof(lp_message) * FONT_WIDTH), FONT_HEIGHT * 4); disp.printf("WAIT!"); disp.locate((sizeof(rt_message) * FONT_WIDTH), FONT_HEIGHT * 2); disp.printf("TOOK!"); /* put a dumming process in order to consume CPU */ for(int i = 0; i < 0x3FFFFFF; i++) (void)0; rt_sema.wait(); } }
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():
/** * @brief normal prio task function */ static void np_task(void) { disp.locate(0, FONT_HEIGHT * 4); disp.printf(lp_message); disp.locate((sizeof(lp_message) * FONT_WIDTH), FONT_HEIGHT * 4); disp.printf("RUNN!"); disp.locate((sizeof(rt_message) * FONT_WIDTH), FONT_HEIGHT * 2); disp.printf("WAIT!"); disp.locate((sizeof(hp_message) * FONT_WIDTH), FONT_HEIGHT * 3); disp.printf("WAIT!"); for(;;) { rt_sema.release(); disp.locate((sizeof(rt_message) * FONT_WIDTH), FONT_HEIGHT * 2); disp.printf("WAIT!"); disp.locate((sizeof(lp_message) * FONT_WIDTH), FONT_HEIGHT * 4); disp.printf("RUNN!"); Thread::wait(1000); hp_sema.release(); disp.locate((sizeof(hp_message) * FONT_WIDTH), FONT_HEIGHT * 3); disp.printf("WAIT!"); disp.locate((sizeof(lp_message) * FONT_WIDTH), FONT_HEIGHT * 4); disp.printf("RUNN!"); Thread::wait(1000); } }
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.
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!
Referência de uso da classe Semaphore do mbed OS, clique aqui .
Meu repositório no mbed contendo o projeto exemplo completo, clique aqui.
<< Como utilizar as Threads no mbed OSComo utilizar os semáforos para compartilhar recursos no mbed OS >>