Mutex e sincronização com pthreads
1. Introdução à Sincronização em Programação Concorrente
Quando múltiplas threads acessam dados compartilhados simultaneamente, surge o problema da condição de corrida (race condition). O resultado da execução depende da ordem não determinística de acesso aos recursos. Considere duas threads incrementando uma variável global contador:
// Código problemático sem sincronização
int contador = 0;
void* incrementa(void* arg) {
for (int i = 0; i < 100000; i++) {
contador++; // Operação não atômica!
}
return NULL;
}
A operação contador++ não é atômica: ela envolve ler o valor, incrementar e escrever de volta. Sem sincronização, o valor final pode ser bem menor que 200.000. A sincronização é essencial para garantir consistência dos dados.
O pthreads oferece diversas primitivas de sincronização: mutexes, variáveis de condição, barreiras, spinlocks e semáforos. Este artigo foca nos mecanismos mais utilizados.
2. Mutex: Exclusão Mútua
Um mutex (mutual exclusion) é como uma chave que dá acesso exclusivo a um recurso. Apenas uma thread pode "segurar" o mutex por vez. As operações fundamentais são:
#include <pthread.h>
pthread_mutex_t mutex; // Declaração
// Inicialização
pthread_mutex_init(&mutex, NULL);
// Lock (trava)
pthread_mutex_lock(&mutex);
// Seção crítica - apenas uma thread executa aqui
pthread_mutex_unlock(&mutex); // Destrava
// Destruição
pthread_mutex_destroy(&mutex);
O exemplo do contador corrigido:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // Inicialização estática
int contador = 0;
void* incrementa_seguro(void* arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&mutex);
contador++;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
3. Mutex com Atributos Avançados
Mutexes podem ser configurados com atributos para comportamentos específicos:
pthread_mutexattr_t attr;
pthread_mutex_t mutex_recursivo;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex_recursivo, &attr);
pthread_mutexattr_destroy(&attr);
Tipos de mutex:
- PTHREAD_MUTEX_NORMAL: deadlock se a mesma thread tentar lock novamente
- PTHREAD_MUTEX_RECURSIVE: permite que a mesma thread faça lock múltiplas vezes
- PTHREAD_MUTEX_ERRORCHECK: detecta tentativas de lock inválidas
- PTHREAD_MUTEX_DEFAULT: comportamento dependente da implementação
Mutexes estáticos são inicializados com PTHREAD_MUTEX_INITIALIZER, enquanto os dinâmicos exigem pthread_mutex_init().
4. Sincronização com Variáveis de Condição
Variáveis de condição (pthread_cond_t) permitem que threads aguardem por uma condição específica. Elas sempre trabalham em conjunto com um mutex.
Padrão produtor-consumidor:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int dado_disponivel = 0;
void* produtor(void* arg) {
pthread_mutex_lock(&mutex);
// Produz o dado...
dado_disponivel = 1;
pthread_cond_signal(&cond); // Notifica uma thread esperando
pthread_mutex_unlock(&mutex);
return NULL;
}
void* consumidor(void* arg) {
pthread_mutex_lock(&mutex);
while (!dado_disponivel) { // Sempre usar while, nunca if!
pthread_cond_wait(&cond, &mutex); // Libera mutex e espera
}
// Consome o dado...
dado_disponivel = 0;
pthread_mutex_unlock(&mutex);
return NULL;
}
pthread_cond_broadcast() acorda todas as threads esperando, útil quando múltiplas threads podem prosseguir.
5. Barreiras e Outros Mecanismos de Sincronização
Barreiras (pthread_barrier_t) sincronizam threads em um ponto específico:
pthread_barrier_t barreira;
pthread_barrier_init(&barreira, NULL, 3); // 3 threads
void* tarefa(void* arg) {
// Fase 1
pthread_barrier_wait(&barreira); // Aguarda todas as 3 threads
// Fase 2 - todas continuam juntas
return NULL;
}
Spinlocks são adequados para seções críticas muito curtas, pois ficam em loop ativo (busy-waiting) em vez de bloquear a thread:
pthread_spinlock_t spin;
pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);
pthread_spin_lock(&spin);
// Seção crítica muito curta
pthread_spin_unlock(&spin);
Comparação: Mutex é a escolha padrão para maioria dos casos. Semáforos contam recursos disponíveis. Barreiras sincronizam fases de execução. Spinlocks só devem ser usados quando o tempo de espera é previsivelmente mínimo.
6. Boas Práticas e Armadilhas Comuns
Deadlocks ocorrem quando threads esperam por recursos que outras threads seguram:
// Exemplo de deadlock
Thread A: lock(mutex1) -> lock(mutex2)
Thread B: lock(mutex2) -> lock(mutex1)
Prevenção: sempre adquirir locks na mesma ordem global.
Granularidade de lock: Um único mutex global simplifica, mas reduz paralelismo. Múltiplos mutexes aumentam desempenho, mas exigem cuidado com deadlocks.
Starvation acontece quando uma thread nunca consegue acesso ao recurso. Livelock é similar a deadlock, mas as threads continuam mudando de estado sem progredir. Use algoritmos justos e evite dependências cíclicas.
7. Exemplo Prático: Contador Compartilhado com Múltiplas Threads
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int contador = 0;
const int LIMITE = 5;
// Thread que incrementa e notifica
void* produtor(void* arg) {
for (int i = 0; i < 3; i++) {
pthread_mutex_lock(&mutex);
contador++;
printf("Produtor: contador = %d\n", contador);
pthread_cond_signal(&cond); // Acorda consumidor
pthread_mutex_unlock(&mutex);
sleep(1);
}
return NULL;
}
// Thread que aguarda contador atingir limite
void* consumidor(void* arg) {
pthread_mutex_lock(&mutex);
while (contador < LIMITE) {
printf("Consumidor: aguardando... (contador = %d)\n", contador);
pthread_cond_wait(&cond, &mutex);
}
printf("Consumidor: contador atingiu %d! Prosseguindo.\n", contador);
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, produtor, NULL);
pthread_create(&t2, NULL, consumidor, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
Saída esperada:
Produtor: contador = 1
Consumidor: aguardando... (contador = 1)
Produtor: contador = 2
Consumidor: aguardando... (contador = 2)
Produtor: contador = 3
Consumidor: aguardando... (contador = 3)
Produtor: contador = 4
Consumidor: aguardando... (contador = 4)
Produtor: contador = 5
Consumidor: contador atingiu 5! Prosseguindo.
8. Conclusão e Próximos Passos
Mutexes e variáveis de condição são fundamentais para programação concorrente segura em C com pthreads. Dominar esses mecanismos permite criar aplicações multi-thread robustas sem condições de corrida ou deadlocks.
Para depuração, ferramentas como Valgrind (com --tool=helgrind) e ThreadSanitizer (-fsanitize=thread) ajudam a detectar problemas de concorrência.
Aprofunde-se em tópicos como comunicação entre processos com pipes e sockets, pools de threads, e algoritmos lock-free para cenários de alto desempenho.
Referências
- The Open Group Base Specifications - pthread_mutex_lock — Documentação oficial das funções de mutex do POSIX
- Linux man pages: pthread_mutex_init(3) — Manual completo de inicialização e tipos de mutex
- POSIX Threads Programming (Lawrence Livermore National Laboratory) — Tutorial abrangente sobre pthreads com exemplos práticos
- IBM Developer: Thread synchronization with pthreads — Artigo técnico sobre sincronização com mutex, condições e barreiras
- The Deadlock Empire (Interactive Guide) — Jogo interativo para aprender sobre deadlocks e sincronização
- GNU C Library: POSIX Threads — Documentação da glibc sobre implementação de pthreads