A programação multitarefa traz diversos desafios, e um dos principais é a sincronização de tarefas que acessam recursos compartilhados. Quando múltiplas threads ou processos precisam interagir com os mesmos dados ou dispositivos, é essencial garantir que essa interação ocorra de forma segura, evitando condições de corrida e corrupção de dados.
Para isso, três técnicas são amplamente utilizadas: mutex, semáforos e a suspensão do escalonador. Cada uma dessas abordagens tem seu melhor momento de aplicação e, se usada de maneira inadequada, pode levar a problemas de desempenho ou até mesmo a falhas difíceis de depurar. Neste artigo, exploraremos quando e como usar cada uma delas, incluindo exemplos práticos para facilitar a compreensão.
Mutex: Exclusão Mútua para Recursos Compartilhados
O mutex (mutual exclusion) é um mecanismo utilizado para garantir que apenas uma tarefa por vez tenha acesso a um recurso compartilhado. Ele funciona como um cadeado: a primeira tarefa que o adquire pode continuar a execução, enquanto as demais precisam aguardar sua liberação.
Quando Usar Mutex
- Quando há um recurso que não pode ser acessado simultaneamente por mais de uma tarefa.
- Para garantir consistência de dados ao modificar variáveis compartilhadas.
- Em sistemas operacionais com preempção habilitada, onde uma tarefa pode ser interrompida a qualquer momento.
Quando Evitar Mutex
- Quando há altíssima concorrência, pois as tarefas podem passar muito tempo bloqueadas, reduzindo o desempenho.
- Quando um recurso pode ser acessado de forma não exclusiva, ou seja, múltiplas tarefas podem lê-lo sem problemas.
- Quando há risco de deadlocks, onde múltiplas tarefas ficam bloqueadas esperando a liberação de mutexes que nunca ocorrerão.
Exemplo de Uso de Mutex no STM32F411RE com FreeRTOS
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "stm32f4xx.h"
SemaphoreHandle_t xMutex;
int shared_resource = 0;
void Task1(void *pvParameters) {
while(1) {
if (xSemaphoreTake(xMutex, portMAX_DELAY)) {
shared_resource++;
printf("Task1: Recurso compartilhado = %d\n", shared_resource);
xSemaphoreGive(xMutex);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void Task2(void *pvParameters) {
while(1) {
if (xSemaphoreTake(xMutex, portMAX_DELAY)) {
shared_resource--;
printf("Task2: Recurso compartilhado = %d\n", shared_resource);
xSemaphoreGive(xMutex);
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
int main() {
xMutex = xSemaphoreCreateMutex();
if (xMutex != NULL) {
xTaskCreate(Task1, "Task 1", 128, NULL, 1, NULL);
xTaskCreate(Task2, "Task 2", 128, NULL, 1, NULL);
vTaskStartScheduler()
}
while(1);
}
Semáforos: Controle de Fluxo e Contagem de Recursos
Os semáforos são estruturas mais flexíveis que os mutexes e podem ser usados para diferentes propósitos. Eles operam com um contador interno, que pode permitir múltiplas tarefas acessando um mesmo recurso, dependendo da configuração.
Quando Usar Semáforos
- Para controlar múltiplas instâncias de um recurso compartilhado, como um conjunto de conexões disponíveis em um servidor.
- Para sincronizar tarefas, permitindo que uma aguarde a outra concluir uma operação antes de continuar.
- Em sistemas embarcados onde múltiplas tarefas precisam se comunicar sem causar bloqueios desnecessários.
Quando Evitar Semáforos
- Quando há necessidade de exclusão mútua. Um semáforo mal configurado pode permitir que múltiplas tarefas acessem um recurso crítico simultaneamente.
- Quando o código não foi planejado corretamente, podendo levar a inconsistências difíceis de depurar.
- Quando há possibilidade de inversão de prioridade, onde tarefas de baixa prioridade podem atrasar a execução de tarefas mais importantes.
Exemplo de Uso de Semáforo no STM32F411RE com FreeRTOS
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "stm32f4xx.h"
SemaphoreHandle_t xSemaphore;
int shared_counter = 0;
void Task1(void *pvParameters) {
while(1) {
if (xSemaphoreTake(xSemaphore, portMAX_DELAY)) {
shared_counter++;
printf("Task1: Contador = %d\n", shared_counter);
xSemaphoreGive(xSemaphore);
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void Task2(void *pvParameters) {
while(1) {
if (xSemaphoreTake(xSemaphore, portMAX_DELAY)) {
shared_counter--;
printf("Task2: Contador = %d\n", shared_counter);
xSemaphoreGive(xSemaphore);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
int main() {
xSemaphore = xSemaphoreCreateBinary();
xSemaphoreGive(xSemaphore);
if (xSemaphore != NULL) {
xTaskCreate(Task1, "Task 1", 128, NULL, 1, NULL);
xTaskCreate(Task2, "Task 2", 128, NULL, 1, NULL);
vTaskStartScheduler();
}
while(1);
}
Suspensão do Escalonador: Controle Total com Custo Alto
A suspensão do escalonador (scheduler lock) é uma abordagem mais extrema, onde o sistema operacional pausa a troca de tarefas, garantindo que a tarefa em execução complete seu trabalho sem interrupções.
Quando Usar a Suspensão do Escalonador
- Quando uma operação crítica precisa ser executada em um curto período de tempo e não pode ser interrompida.
- Para seções de código extremamente curtas e atômicas, onde o impacto da suspensão do escalonador é mínimo.
- Em sistemas bare-metal sem suporte a mutexes ou semáforos.
Quando Evitar a Suspensão do Escalonador
- Quando há tarefas de tempo real que precisam responder rapidamente a eventos externos.
- Se o código protegido for muito longo, pois a suspensão prolongada do escalonador pode causar degradação significativa no desempenho do sistema.
- Em sistemas onde múltiplas CPUs são usadas, pois suspender o escalonador em um núcleo não impede que outros núcleos executem tarefas concorrentes.
Exemplo de Suspensão do Escalonador no STM32F411RE com FreeRTOS
#include "FreeRTOS.h"
#include "task.h"
void CriticalTask(void *pvParameters) {
while(1) {
vTaskSuspendAll(); // Suspende o escalonador
// Código crítico
printf("Executando tarefa crítica\n");
xTaskResumeAll(); // Retoma o escalonador
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
int main() {
xTaskCreate(CriticalTask, "Critical Task", 128, NULL, 1, NULL);
vTaskStartScheduler();
while(1);
}
Abordagem para Dispositivos I2C e SPI
Os protocolos de comunicação como I2C e SPI são amplamente utilizados em sistemas embarcados para comunicação entre microcontroladores e periféricos. A abordagem correta para controle de concorrência depende do tipo de comunicação e do contexto de uso.
I2C com Mutex no STM32F411RE
O protocolo I2C é um barramento compartilhado onde múltiplos dispositivos podem estar conectados ao mesmo controlador. Como apenas um dispositivo pode realizar a comunicação por vez, o uso de mutex é essencial para evitar colisões entre tarefas concorrentes que tentam acessar o barramento ao mesmo tempo.
#include "stm32f4xx_hal.h"
#include "FreeRTOS.h"
#include "semphr.h"
I2C_HandleTypeDef hi2c1;
SemaphoreHandle_t i2cMutex;
void I2C_Write(uint16_t devAddress, uint8_t *data, uint16_t size) {
if (xSemaphoreTake(i2cMutex, portMAX_DELAY)) {
HAL_I2C_Master_Transmit(&hi2c1, devAddress, data, size, HAL_MAX_DELAY);
xSemaphoreGive(i2cMutex);
}
}
SPI com Mutex no STM32F411RE
O protocolo SPI geralmente é mais rápido que o I2C, mas também pode sofrer de concorrência quando múltiplas tarefas tentam acessá-lo simultaneamente. O uso de mutex impede que duas tarefas interfiram uma na outra enquanto utilizam o barramento SPI.
#include "stm32f4xx_hal.h"
#include "FreeRTOS.h"
#include "semphr.h"
SPI_HandleTypeDef hspi1;
SemaphoreHandle_t spiMutex;
void SPI_Write(uint8_t *data, uint16_t size) {
if (xSemaphoreTake(spiMutex, portMAX_DELAY)) {
HAL_SPI_Transmit(&hspi1, data, size, HAL_MAX_DELAY);
xSemaphoreGive(spiMutex);
}
}
Melhor Abordagem
- I2C: Como é um barramento compartilhado entre múltiplos dispositivos, usar um mutex é essencial para evitar colisões.
- SPI: Em ambientes multitarefa, o uso de mutex também é recomendado para evitar que diferentes tarefas tentem acessar o mesmo barramento ao mesmo tempo.
- Semáforos podem ser usados para sincronizar eventos, como a recepção de dados em interrupções.
Essas práticas garantem que a comunicação com dispositivos externos ocorra de maneira segura e eficiente, sem causar falhas no sistema.
Conclusão
Escolher a técnica correta para o controle de concorrência é essencial para garantir a estabilidade e o desempenho de sistemas multitarefa. Como visto ao longo deste artigo:
- Mutexes são ideais para garantir exclusão mútua, prevenindo que múltiplas tarefas modifiquem simultaneamente um mesmo recurso compartilhado.
- Semáforos são úteis tanto para controle de múltiplas instâncias de um recurso quanto para sincronização de tarefas.
- A suspensão do escalonador deve ser usada com cuidado, sendo útil apenas em situações críticas onde uma interrupção durante a execução pode causar problemas graves.
Quando se trata de dispositivos como I2C e SPI, a escolha de mutexes para proteger o barramento e garantir a integridade da comunicação é a abordagem mais recomendada, enquanto semáforos podem ser úteis para coordenar eventos assíncronos.
Entender essas técnicas e aplicá-las corretamente resulta em sistemas mais eficientes, confiáveis e fáceis de manter. O uso adequado de mutexes, semáforos e a suspensão do escalonador garante que aplicações multitarefa operem de maneira fluida, sem impactos negativos na performance do sistema.
Com isso, esperamos que este artigo tenha esclarecido as principais diferenças entre essas abordagens e ajudado a tomar decisões mais informadas no desenvolvimento de sistemas embarcados e aplicações multitarefa.