Aula 03 — Programação concorrente, modelos de concorrência e controle de concorrência
Bacharelado em Ciência da Computação, Instituto Federal Sul-rio-grandense, 2026
Aula 03 — Programação concorrente, arquiteturas e modelos de concorrência, processos, threads, middlewares e introdução ao controle de concorrência
Objetivo
Apresentar os fundamentos da programação concorrente em sistemas operacionais e em sistemas de software em geral. Discutir arquiteturas e modelos de concorrência, a relação entre processos, threads e middlewares, e introduzir os problemas centrais de controle de concorrência: condição de corrida, região crítica e exclusão mútua.
Provocações iniciais (para abrir a discussão)
- O que significa dizer que um programa é concorrente?
- Concorrência é a mesma coisa que paralelismo?
- Se dois threads compartilham memória, o que pode dar errado?
- Por que um middleware existe entre a aplicação e a rede/sistema operacional?
- O que muda quando deixamos de pensar em “sequência de instruções” e passamos a pensar em “múltiplos fluxos de execução”?
- Como o sistema operacional, a linguagem e a biblioteca afetam o modelo de concorrência disponível?
(Solicite respostas curtas em pares e retome as respostas ao longo da aula.)
Conteúdo (definições e itens a cobrir)
Programação concorrente
Programação concorrente é a construção de software em que múltiplas tarefas podem progredir em sobreposição temporal. Essas tarefas podem executar em um único núcleo por interleaving, ou em múltiplos núcleos de forma paralela.
A concorrência aparece em diversos contextos:
- múltiplos processos no mesmo sistema;
- múltiplas threads dentro de um processo;
- múltiplas requisições atendidas por um servidor;
- múltiplos eventos sendo tratados ao mesmo tempo.
A ideia central não é apenas “executar mais rápido”, mas organizar corretamente o trabalho em partes que podem progredir de forma independente.
Arquiteturas e modelos de concorrência
A forma como a concorrência é implementada depende da arquitetura do sistema e do modelo de software adotado.
Modelos clássicos:
- Concorrência por processos: cada tarefa é isolada em seu próprio processo. Há separação de memória e maior proteção, mas comunicação costuma ser mais custosa.
- Concorrência por threads: várias tarefas compartilham o mesmo espaço de endereçamento. A comunicação é mais direta, mas aumenta o risco de conflitos.
- Modelo orientado a eventos: um único fluxo principal coordena eventos assíncronos. Muito usado em interfaces, servidores e I/O não bloqueante.
- Modelo cliente-servidor: clientes enviam requisições e servidores tratam múltiplas conexões concorrentes.
- Modelo produtor-consumidor: uma parte produz trabalho, outra consome, com fila intermediária e sincronização.
- Modelo mestre-escravo / worker pool: uma entidade distribui trabalho para múltiplos trabalhadores.
- Modelo fork-join: uma tarefa principal se divide em subtarefas e depois sincroniza os resultados.
Arquiteturas multicore e multiprocessadas tornam a concorrência ainda mais relevante, mas os problemas lógicos já aparecem mesmo em um único núcleo.
Processos
Um processo é uma instância de programa em execução. Ele possui, em geral:
- espaço de endereçamento próprio;
- contador de programa;
- registradores;
- pilha;
- recursos associados, como arquivos abertos.
Concorrência entre processos tende a favorecer isolamento e proteção. Quando processos precisam compartilhar informação, a comunicação ocorre por mecanismos específicos, como pipes, memória compartilhada, sockets ou mensagens.
Threads
Uma thread é um fluxo de execução dentro de um processo. Threads do mesmo processo compartilham:
- memória;
- arquivos abertos;
- código do programa;
- variáveis globais.
Cada thread mantém seu próprio contexto de execução, como:
- contador de programa;
- registradores;
- pilha.
Threads são úteis quando diferentes partes de um programa precisam avançar de forma concorrente e compartilhar dados com frequência. Em contrapartida, exigem controle rigoroso sobre acessos simultâneos.
Middleware
Middleware é uma camada de software intermediária entre aplicações e serviços de mais baixo nível, como o sistema operacional, a rede ou sistemas distribuídos.
Ele existe para simplificar a concorrência e a comunicação entre componentes. Exemplos conceituais de middleware incluem:
- bibliotecas de threads;
- frameworks de servidor;
- RPC e chamadas remotas;
- bibliotecas para filas, mensageria e sincronização;
- APIs que abstraem rede e concorrência.
O middleware ajuda a esconder detalhes de baixo nível, mas não elimina os problemas de concorrência. Ele apenas os organiza em um nível mais alto de abstração.
Concorrência vs paralelismo
- Concorrência: múltiplas tarefas progridem de forma sobreposta no tempo.
- Paralelismo: múltiplas tarefas executam literalmente ao mesmo tempo.
Um programa pode ser concorrente sem ser paralelo. Em um sistema de núcleo único, duas threads podem se alternar rapidamente e parecer simultâneas. Em um sistema multicore, além da concorrência, pode haver paralelismo real.
Controle de concorrência
Quando múltiplos fluxos acessam dados compartilhados, surgem problemas como:
- condição de corrida;
- inconsistência de dados;
- escrita perdida;
- observação de estados intermediários.
O controle de concorrência busca impedir que essas falhas aconteçam. Os conceitos básicos introduzidos aqui são:
- região crítica: trecho que acessa recurso compartilhado;
- exclusão mútua: garantia de que apenas um fluxo entra por vez;
- sincronização: coordenação da ordem de execução entre threads ou processos.
Condição de corrida
Uma condição de corrida ocorre quando o resultado de um programa depende da ordem não determinística em que operações concorrentes acontecem.
Exemplo conceitual:
- duas threads leem uma variável compartilhada;
- ambas calculam um novo valor;
- ambas escrevem de volta;
- uma escrita sobrescreve a outra.
O resultado final deixa de ser previsível.
Região crítica
Uma região crítica é o trecho do código em que um recurso compartilhado é lido ou modificado e que, portanto, precisa de coordenação para evitar conflito.
Exemplos:
- incremento de contador global;
- atualização de fila compartilhada;
- acesso a estrutura de dados comum;
- escrita simultânea em um arquivo ou buffer.
Exclusão mútua
Exclusão mútua é a propriedade que impede a entrada simultânea de múltiplos fluxos na mesma região crítica.
Em uma solução correta, também se espera:
- progresso;
- espera limitada;
- ausência de bloqueio indevido;
- manutenção da consistência do estado compartilhado.
Exemplos e simulações (códigos)
1) Exemplo mínimo de concorrência com threads (C / pthreads)
/* concorrencia_basica.c
compile: gcc -Wall -std=c99 -pthread concorrencia_basica.c -o concorrencia_basica
*/
#include <stdio.h>
#include <pthread.h>
#define N 2
void *trabalhador(void *arg) {
int id = *(int *)arg;
printf("Thread %d executando\n", id);
return NULL;
}
int main(void) {
pthread_t t[N];
int id[N] = {1, 2};
for (int i = 0; i < N; i++) {
pthread_create(&t[i], NULL, trabalhador, &id[i]);
}
for (int i = 0; i < N; i++) {
pthread_join(t[i], NULL);
}
return 0;
}
2) Exemplo de condição de corrida (C / pthreads)
/* corrida_contador.c
compile: gcc -Wall -std=c99 -pthread corrida_contador.c -o corrida_contador
*/
#include <stdio.h>
#include <pthread.h>
#define NITER 1000000
long long contador = 0;
void *incrementa(void *arg) {
for (long long i = 0; i < NITER; i++) {
contador++;
}
return NULL;
}
int main(void) {
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 = %lld (esperado %d)\n", contador, 2 * NITER);
return 0;
}
3) Correção com exclusão mútua (C / pthread_mutex)
/* contador_mutex.c
compile: gcc -Wall -std=c99 -pthread contador_mutex.c -o contador_mutex
*/
#include <stdio.h>
#include <pthread.h>
#define NITER 1000000
long long contador = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *incrementa(void *arg) {
for (long long i = 0; i < NITER; i++) {
pthread_mutex_lock(&lock);
contador++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main(void) {
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 = %lld (esperado %d)\n", contador, 2 * NITER);
return 0;
}
4) Exemplo simples de produtor-consumidor com fila circular e mutex
/* produtor_consumidor_simples.c
compile: gcc -Wall -std=c99 -pthread produtor_consumidor_simples.c -o pc
*/
#include <stdio.h>
#include <pthread.h>
#define TAM 5
#define NITEMS 10
int buffer[TAM];
int in = 0, out = 0, count = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void produzir(int item) {
buffer[in] = item;
in = (in + 1) % TAM;
count++;
}
int consumir(void) {
int item = buffer[out];
out = (out + 1) % TAM;
count--;
return item;
}
void *produtor(void *arg) {
for (int i = 1; i <= NITEMS; i++) {
pthread_mutex_lock(&lock);
if (count < TAM) {
produzir(i);
printf("produziu %d\n", i);
}
pthread_mutex_unlock(&lock);
}
return NULL;
}
void *consumidor(void *arg) {
for (int i = 1; i <= NITEMS; i++) {
pthread_mutex_lock(&lock);
if (count > 0) {
int item = consumir();
printf("consumiu %d\n", item);
}
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main(void) {
pthread_t p, c;
pthread_create(&p, NULL, produtor, NULL);
pthread_create(&c, NULL, consumidor, NULL);
pthread_join(p, NULL);
pthread_join(c, NULL);
return 0;
}
5) Exemplo conceitual de concorrência por processos (fork)
/* processos_fork.c
compile: gcc -Wall -std=c99 processos_fork.c -o processos_fork
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid == 0) {
printf("Processo filho executando\n");
} else {
printf("Processo pai executando\n");
wait(NULL);
}
return 0;
}
Exemplos de uso em sala
- Executar
concorrencia_basica.cpara introduzir o tema e mostrar que vários fluxos podem existir no mesmo programa. - Executar
corrida_contador.ce pedir que os estudantes expliquem por que o resultado pode variar. - Executar
contador_mutex.ce comparar com a versão sem proteção. - Usar
produtor_consumidor_simples.cpara discutir que proteção não é só “travar tudo”, mas coordenar acesso a recursos compartilhados. - Usar
processos_fork.cpara contrastar processo e thread.
Questões potenciais de prova
Objetivas
- Qual a diferença entre concorrência e paralelismo?
- O que caracteriza uma condição de corrida?
- O que é uma região crítica?
- Qual a finalidade da exclusão mútua?
- Por que threads compartilham dados com mais facilidade do que processos?
- O que é um middleware?
- Qual é a vantagem de uma thread sobre um processo em comunicação frequente?
Discursivas
- Explique o que é programação concorrente e por que ela aparece naturalmente em sistemas operacionais e aplicações modernas.
- Compare processos, threads e middlewares em termos de isolamento, comunicação e controle de concorrência.
- Explique por que o acesso concorrente a uma variável compartilhada pode gerar inconsistência.
- Defina região crítica, condição de corrida e exclusão mútua, relacionando os três conceitos.
Aplicadas / Implementação
- Dado um trecho de código com duas threads atualizando uma variável global, identifique o problema e mostre uma correção com mutex.
- Explique por que um servidor concorrente frequentemente combina threads e middleware.
- Analise um cenário produtor-consumidor e indique onde surgem pontos de sincronização.
Atividades recomendadas em aula
- Pedir que os estudantes identifiquem, em um código curto, quais variáveis são compartilhadas e onde está a região crítica.
- Rodar a versão com e sem mutex e observar o resultado.
- Discutir em dupla a diferença entre processo, thread e middleware.
- Fechar com uma síntese oral: “o que precisa ser protegido e por quê?”.
Referências
- Tanenbaum, A. S. — Modern Operating Systems, capítulos iniciais sobre processos, threads, concorrência e sincronização.
- Materiais de apoio do curso e documentação de
pthread.
