Threads com pthreads

1. Introdução às Threads e à Biblioteca pthreads

Threads são unidades de execução leves que compartilham o mesmo espaço de endereçamento dentro de um processo. Diferentemente de processos pesados, que possuem contexto independente e exigem troca de contexto custosa, threads permitem concorrência real com menor overhead de criação e comunicação.

As principais vantagens do uso de threads incluem:
- Compartilhamento direto de memória e recursos entre threads do mesmo processo
- Criação e destruição mais rápidas que processos
- Melhor aproveitamento de processadores multicore

A biblioteca POSIX threads (pthreads) é o padrão para programação concorrente em C em sistemas Unix-like. Para utilizá-la, inclua o cabeçalho:

#include <pthread.h>

A compilação requer a flag -lpthread:

gcc programa.c -o programa -lpthread

2. Criação e Execução de Threads

A função pthread_create() cria uma nova thread. Sua assinatura é:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine)(void *), void *arg);

O primeiro parâmetro recebe o identificador da thread criada. O segundo define atributos (NULL para padrão). O terceiro é a função que a thread executará, e o último é o argumento passado para essa função.

Para aguardar o término de uma thread, usamos pthread_join():

int pthread_join(pthread_t thread, void **retval);

Exemplo prático com duas threads paralelas:

#include <stdio.h>
#include <pthread.h>
#include <string.h>

void *imprime_mensagem(void *arg) {
    char *msg = (char *)arg;
    for (int i = 0; i < 5; i++) {
        printf("%s: iteração %d\n", msg, i);
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    char *msg1 = "Thread A";
    char *msg2 = "Thread B";

    pthread_create(&t1, NULL, imprime_mensagem, (void *)msg1);
    pthread_create(&t2, NULL, imprime_mensagem, (void *)msg2);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("Ambas as threads finalizaram.\n");
    return 0;
}

3. Identificação e Gerenciamento de Threads

Cada thread possui um identificador único obtido com pthread_self():

pthread_t tid = pthread_self();

Para comparar IDs, use pthread_equal():

if (pthread_equal(tid, outra_tid)) {
    printf("Mesma thread\n");
}

O cancelamento de threads é feito com pthread_cancel(), mas a thread alvo só será cancelada ao atingir um ponto de cancelamento (como pthread_testcancel() ou chamadas bloqueantes):

pthread_cancel(tid);

4. Mutex: Exclusão Mútua para Dados Compartilhados

Quando múltiplas threads acessam dados compartilhados sem sincronização, ocorre uma condição de corrida (race condition). O mutex (mutual exclusion) resolve esse problema.

Inicialização e destruição:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  // estática
// ou dinâmica:
pthread_mutex_init(&mutex, NULL);
pthread_mutex_destroy(&mutex);

Lock e unlock:

pthread_mutex_lock(&mutex);
// região crítica
pthread_mutex_unlock(&mutex);

Exemplo de proteção de variável compartilhada:

#include <stdio.h>
#include <pthread.h>

int contador = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *incrementa(void *arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);
        contador++;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, incrementa, NULL);
    pthread_create(&t2, NULL, incrementa, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("Contador: %d (esperado: 200000)\n", contador);
    return 0;
}

5. Variáveis de Condição para Sincronização Avançada

Variáveis de condição permitem que threads esperem por uma condição específica e sejam notificadas quando ela ocorrer. Funcionam sempre associadas a um mutex.

Funções principais:

pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
pthread_cond_signal(pthread_cond_t *cond);   // desperta uma thread
pthread_cond_broadcast(pthread_cond_t *cond); // desperta todas

Exemplo produtor-consumidor simples:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define BUFFER_SIZE 5

int buffer[BUFFER_SIZE];
int count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_produtor = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond_consumidor = PTHREAD_COND_INITIALIZER;

void *produtor(void *arg) {
    int item = 0;
    while (1) {
        pthread_mutex_lock(&mutex);
        while (count == BUFFER_SIZE) {
            pthread_cond_wait(&cond_produtor, &mutex);
        }
        buffer[count++] = item++;
        printf("Produziu: %d (count=%d)\n", item-1, count);
        pthread_cond_signal(&cond_consumidor);
        pthread_mutex_unlock(&mutex);
        usleep(100000);
    }
    return NULL;
}

void *consumidor(void *arg) {
    while (1) {
        pthread_mutex_lock(&mutex);
        while (count == 0) {
            pthread_cond_wait(&cond_consumidor, &mutex);
        }
        int item = buffer[--count];
        printf("Consumiu: %d (count=%d)\n", item, count);
        pthread_cond_signal(&cond_produtor);
        pthread_mutex_unlock(&mutex);
        usleep(200000);
    }
    return NULL;
}

int main() {
    pthread_t prod, cons;
    pthread_create(&prod, NULL, produtor, NULL);
    pthread_create(&cons, NULL, consumidor, NULL);
    pthread_join(prod, NULL);
    pthread_join(cons, NULL);
    return 0;
}

6. Atributos de Thread e Configurações Específicas

A estrutura pthread_attr_t permite configurar características da thread antes da criação:

pthread_attr_t attr;
pthread_attr_init(&attr);

Joinable vs. Detached: Por padrão, threads são joinable (necessitam pthread_join). Threads detached liberam recursos automaticamente ao terminar:

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

Outras configurações incluem tamanho da pilha:

size_t stacksize;
pthread_attr_getstacksize(&attr, &stacksize);
pthread_attr_setstacksize(&attr, 1024 * 1024);  // 1 MB

7. Tratamento de Erros e Boas Práticas

Todas as funções pthread retornam 0 em caso de sucesso ou um código de erro. Sempre verifique:

if (pthread_create(&tid, NULL, funcao, NULL) != 0) {
    perror("Erro ao criar thread");
    exit(1);
}

Deadlocks ocorrem quando duas ou mais threads esperam indefinidamente por recursos que a outra possui. Para prevenir:
- Estabeleça uma ordem global para aquisição de locks
- Use pthread_mutex_trylock() para tentativas não bloqueantes
- Evite aninhamento desnecessário de mutexes

Boas práticas essenciais:
- Sempre destrua mutexes e variáveis de condição após o uso
- Use pthread_cleanup_push/pop para garantir liberação de recursos
- Documente claramente quais dados cada mutex protege

8. Exemplo Completo: Threads com Mutex e Condição

Implementação de um buffer circular compartilhado entre threads produtoras e consumidoras:

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

#define BUFFER_SIZE 10
#define NUM_ITENS 20

typedef struct {
    int buffer[BUFFER_SIZE];
    int head, tail, count;
    pthread_mutex_t mutex;
    pthread_cond_t cheio, vazio;
} BufferCircular;

void buffer_init(BufferCircular *buf) {
    buf->head = buf->tail = buf->count = 0;
    pthread_mutex_init(&buf->mutex, NULL);
    pthread_cond_init(&buf->cheio, NULL);
    pthread_cond_init(&buf->vazio, NULL);
}

void buffer_destroy(BufferCircular *buf) {
    pthread_mutex_destroy(&buf->mutex);
    pthread_cond_destroy(&buf->cheio);
    pthread_cond_destroy(&buf->vazio);
}

void buffer_insere(BufferCircular *buf, int valor) {
    pthread_mutex_lock(&buf->mutex);
    while (buf->count == BUFFER_SIZE) {
        pthread_cond_wait(&buf->cheio, &buf->mutex);
    }
    buf->buffer[buf->tail] = valor;
    buf->tail = (buf->tail + 1) % BUFFER_SIZE;
    buf->count++;
    printf("Inseriu: %d (count=%d)\n", valor, buf->count);
    pthread_cond_signal(&buf->vazio);
    pthread_mutex_unlock(&buf->mutex);
}

int buffer_remove(BufferCircular *buf) {
    pthread_mutex_lock(&buf->mutex);
    while (buf->count == 0) {
        pthread_cond_wait(&buf->vazio, &buf->mutex);
    }
    int valor = buf->buffer[buf->head];
    buf->head = (buf->head + 1) % BUFFER_SIZE;
    buf->count--;
    printf("Removeu: %d (count=%d)\n", valor, buf->count);
    pthread_cond_signal(&buf->cheio);
    pthread_mutex_unlock(&buf->mutex);
    return valor;
}

void *produtor(void *arg) {
    BufferCircular *buf = (BufferCircular *)arg;
    for (int i = 0; i < NUM_ITENS; i++) {
        buffer_insere(buf, i);
        usleep(50000);
    }
    return NULL;
}

void *consumidor(void *arg) {
    BufferCircular *buf = (BufferCircular *)arg;
    for (int i = 0; i < NUM_ITENS; i++) {
        int valor = buffer_remove(buf);
        usleep(100000);
    }
    return NULL;
}

int main() {
    BufferCircular buf;
    buffer_init(&buf);

    pthread_t prod, cons;
    pthread_create(&prod, NULL, produtor, &buf);
    pthread_create(&cons, NULL, consumidor, &buf);

    pthread_join(prod, NULL);
    pthread_join(cons, NULL);

    buffer_destroy(&buf);
    printf("Programa finalizado.\n");
    return 0;
}

Para compilar e executar:

gcc buffer_circular.c -o buffer_circular -lpthread
./buffer_circular

Referências

  • The Open Group Base Specifications - pthread.h — Documentação oficial da especificação POSIX para pthreads, com definições completas das funções e tipos.
  • Linux man pages - pthreads — Páginas de manual detalhadas sobre a implementação de pthreads no Linux, incluindo exemplos e notas sobre comportamento.
  • POSIX Threads Programming (LLNL) — Tutorial abrangente do Lawrence Livermore National Laboratory sobre programação com pthreads, cobrindo desde conceitos básicos até tópicos avançados.
  • GNU C Library - Threads — Documentação da glibc sobre suporte a threads, com detalhes de implementação e exemplos práticos.
  • Beej's Guide to Unix IPC - Threads — Guia prático e acessível sobre comunicação entre processos e threads em Unix, com foco em exemplos funcionais de pthreads.