- Definição e Propriedades das Threads
Threads são unidades de execução leves dentro de um processo, atuando como a unidade básica de agendamento da CPU. Um processo pode conter múltiplas threads, que compartilham recursos como espaço de endereçamento, descritores de arquivo e mecanismos de tratamento de sinais. Cada thread mantém recursos independentes, incluindo pilha, contador de programa e registradores.
1.1 Contraste entre Threads e Processos
Processos são a unidade fundamental de alocação de recursos no sistema operacional, possuindo espaços de endereçamento isolados, o que implica custos elevados na troca de contexto. Threads, por outro lado, são unidades de agendamento de CPU que compartilham recursos do processo, permitindo trocas de contexto mais eficientes com sobrecarga reduzida.
1.2 Comportamento de Cópia no Kernel Linux
No Linux, threads são geradas através da syscall clone(), enquanto processos usam fork(). O fork() emprega cópia-em-escrita (COW), replicando todos os recursos, resultando em custos altos. O clone() compartilha recursos principais e copia apenas elementos independentes como pilha e registradores, o que explica a natureza leve das threads.
Exemplo de código ilustrando variáveis compartilhadas e locais:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
int shared_value = 0;
void *task_thread(void *input) {
int local_data = *((int *)input);
printf("Thread %ld: valor inicial compartilhado=%d, dado local=%d\n",
pthread_self(), shared_value, local_data);
shared_value += 1;
local_data *= 2;
printf("Thread %ld: valor compartilhado modificado=%d, dado local modificado=%d\n",
pthread_self(), shared_value, local_data);
return NULL;
}
int main() {
pthread_t thr1, thr2;
int param1 = 8, param2 = 12;
pthread_create(&thr1, NULL, task_thread, ¶m1);
pthread_create(&thr2, NULL, task_thread, ¶m2);
pthread_join(thr1, NULL);
pthread_join(thr2, NULL);
printf("Thread principal: valor final compartilhado=%d\n", shared_value);
return 0;
}
- Métodos de Comunicação entre Threads
Threads, ao compartilharem o espaço de endereçamento do processo, utilizam métodos de comunicação eficientes e diretos.
2.1 Memória Compartilhada
Envolve o uso de recursos do processo como variáveis globais, estáticas ou memória heap, permitindo acesso direto sem cópia de dados. Requer sincronização, como com mutexes, para evitar condições de corrida.
2.2 Pipes (Canais)
Criados via pipe(), permitem comunicação por descritores de arquivo entre threads, ideal para fluxos de bytes simples. É necessário fechar extremidades não utilizadas para evitar bloqueios.
Exemplo de código com pipe:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
int fd[2];
void *writer_task(void *arg) {
const char *data = "Dados da thread escritora!";
close(fd[0]);
write(fd[1], data, strlen(data) + 1);
printf("Thread escritora: dados enviados: %s\n", data);
close(fd[1]);
return NULL;
}
void *reader_task(void *arg) {
char buffer[1024];
close(fd[1]);
read(fd[0], buffer, sizeof(buffer));
printf("Thread leitora: dados recebidos: %s\n", buffer);
close(fd[0]);
return NULL;
}
int main() {
pthread_t tid_wr, tid_rd;
if (pipe(fd) == -1) {
perror("Falha na criação do pipe");
return -1;
}
pthread_create(&tid_wr, NULL, writer_task, NULL);
pthread_create(&tid_rd, NULL, reader_task, NULL);
pthread_join(tid_wr, NULL);
pthread_join(tid_rd, NULL);
return 0;
}
2.3 Filas de Mensagens
Baseadas em APIs POSIX como mq_open(), armazenam e transferem mensagens com prioridades, suportando comunicação assíncrona e formatos estruturados.
2.4 Sinais
Enviados via pthread_kill() e tratados com sigaction(), premitem notificação entre threads. Funções no handler devem ser reentrantes para evitar anomalias.
- Sincronização com Mutex
Mutex é um mecanismo de exclusão mútua que garante acesso único a recursos compartilhados, prevenindo condições de corrida por meio de operações de bloqueio e desbloqueio em seções críticas.
Características incluem exclusividade, operações em pares (lock/unlock) e suporte a bloqueio ou tentativa não bloqueante.
Exemplo de código com lista encadeada protegida por mutex:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
typedef struct Elemento {
int info;
struct Elemento *prox;
} Elemento;
Elemento *inicio = NULL;
pthread_mutex_t candado;
void inserir_elemento(int info) {
Elemento *novo = (Elemento *)malloc(sizeof(Elemento));
novo->info = info;
novo->prox = NULL;
pthread_mutex_lock(&candado);
novo->prox = inicio;
inicio = novo;
pthread_mutex_unlock(&candado);
printf("Elemento inserido: %d\n", info);
}
void *tarefa_insercao(void *arg) {
int base = *((int *)arg);
for (int i = 0; i < 5; i++) {
inserir_elemento(base + i);
}
return NULL;
}
int main() {
pthread_t t1, t2;
int base1 = 10, base2 = 200;
pthread_mutex_init(&candado, NULL);
pthread_create(&t1, NULL, tarefa_insercao, &base1);
pthread_create(&t2, NULL, tarefa_insercao, &base2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
Elemento *atual = inicio;
printf("Conteúdo da lista: ");
while (atual != NULL) {
printf("%d ", atual->info);
Elemento *temp = atual;
atual = atual->prox;
free(temp);
}
printf("\n");
pthread_mutex_destroy(&candado);
return 0;
}
- Sincronização com Variáveis de Condição
Variáveis de condição, usadas com mutexes, implementam o modelo de espera-notificação para evitar espera ocupada, permitindo que threads aguardem condições específicas antes de prosseguir.
Funcionalidades incluem liberação automática do mutex durante espera e modos de sinalização individual ou broadcast. É essencial verificar condições em loops para prevenir despertares falsos.
Exemplo de modelo produtor-consumidor:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define TAMANHO_FILA 4
int fila[TAMANHO_FILA];
int itens = 0;
pthread_mutex_t travamento;
pthread_cond_t cond_produtor;
pthread_cond_t cond_consumidor;
void *produtor(void *arg) {
int dado = 0;
while (1) {
pthread_mutex_lock(&travamento);
while (itens == TAMANHO_FILA) {
printf("Produtor: fila cheia, aguardando...\n");
pthread_cond_wait(&cond_produtor, &travamento);
}
fila[itens++] = dado;
printf("Produtor: produziu %d, itens na fila: %d\n", dado++, itens);
pthread_cond_signal(&cond_consumidor);
pthread_mutex_unlock(&travamento);
usleep(500000);
}
return NULL;
}
void *consumidor(void *arg) {
while (1) {
pthread_mutex_lock(&travamento);
while (itens == 0) {
printf("Consumidor: fila vazia, aguardando...\n");
pthread_cond_wait(&cond_consumidor, &travamento);
}
int dado = fila[--itens];
printf("Consumidor: consumiu %d, itens na fila: %d\n", dado, itens);
pthread_cond_signal(&cond_produtor);
pthread_mutex_unlock(&travamento);
usleep(1000000);
}
return NULL;
}
int main() {
pthread_t prod, cons;
pthread_mutex_init(&travamento, NULL);
pthread_cond_init(&cond_produtor, NULL);
pthread_cond_init(&cond_consumidor, NULL);
pthread_create(&prod, NULL, produtor, NULL);
pthread_create(&cons, NULL, consumidor, NULL);
pthread_join(prod, NULL);
pthread_join(cons, NULL);
pthread_mutex_destroy(&travamento);
pthread_cond_destroy(&cond_produtor);
pthread_cond_destroy(&cond_consumidor);
return 0;
}