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