LPII — Programação Concorrente | Exercício Prático 2 | UFPB — CI
Professor: Carlos Eduardo Batista
| Campo | Informação |
|---|---|
| Matrícula | xxxxxx7380 |
| Cenário escolhido | A — Restaurante Concorrente |
| Linguagem | C (pthreads POSIX) |
A partir dos últimos 4 dígitos da matrícula, M = 7380:
NUM_COZINHEIROS = (7380 % 4) + 2 = 0 + 2 = 2
NUM_FOGOES = (7380 % 3) + 2 = 0 + 2 = 2
NUM_MESAS = ((7380 / 10) % 5) + 4 = 3 + 4 = 7
NUM_GARCONS = ((7380 / 100) % 3) + 2 = 1 + 2 = 3
TEMPO_PREPARO = ((7380 / 100) % 3) + 1 = 1 + 1 = 2 s
TOTAL_PEDIDOS = 7 mesas × 3 pedidos × 2 rodadas = 42
# Compilar
make
# Executar (salva log em resultados/)
make run
# Limpar objetos
make cleanCompilação manual, caso prefira:
# Linux
gcc -Wall -Wextra -pthread -g -o restaurante src/main.c src/sincronizacao.c -lrt
# macOS
gcc -Wall -Wextra -pthread -g -o restaurante src/main.c src/sincronizacao.cAntes de entrar nas decisões técnicas, quero explicar rapidamente o que cada mecanismo de sincronização faz — porque entender isso é o que faz a implementação fazer sentido.
Quando várias threads rodam ao mesmo tempo e todas precisam ler ou escrever na mesma variável, surge o problema de race condition (condição de corrida). Imagine duas threads fazendo contador++ ao mesmo tempo:
Thread A: lê contador = 5
Thread B: lê contador = 5 ← leu antes de A gravar!
Thread A: grava 6
Thread B: grava 6 ← perdemos 1 incremento
O resultado deveria ser 7, mas ficou 6. Isso é uma race condition. Para evitá-la, precisamos de mecanismos de sincronização.
O mutex (mutual exclusion) é o mecanismo mais básico. Ele funciona como um cadeado: só uma thread entra na seção crítica por vez. As outras ficam esperando na fila.
pthread_mutex_lock(&meu_mutex);
/* --- seção crítica: só uma thread aqui por vez --- */
contador++;
pthread_mutex_unlock(&meu_mutex);No projeto usei mutex em três lugares:
- Para proteger o acesso às filas (push e pop no ring buffer)
- Para proteger as estatísticas globais (vários cozinheiros atualizando ao mesmo tempo)
- Para serializar as impressões no terminal (sem mutex, as mensagens de threads diferentes se intercalavam na tela)
O semáforo é mais poderoso que o mutex. Ele guarda um contador interno (N) e funciona assim:
sem_wait(): tenta decrementar N. Se N já é 0, bloqueia (dorme) até alguém incrementar.sem_post(): incrementa N e acorda uma thread bloqueada, se houver.
Usei semáforos em dois contextos diferentes:
Para as filas (padrão produtor-consumidor): cada fila tem dois semáforos:
sem_slots: representa quantos slots livres existem. O produtor fazsem_wait(slots)antes de inserir — se a fila estiver cheia, ele espera.sem_peds: representa quantos itens disponíveis existem. O consumidor fazsem_wait(peds)antes de remover — se a fila estiver vazia, ele dorme.
Produtor: sem_wait(slots) → lock → push → unlock → sem_post(peds)
Consumidor: sem_wait(peds) → lock → pop → unlock → sem_post(slots)
Para os fogões: sem_fogoes começa em 2 (dois fogões disponíveis). Cada cozinheiro faz sem_wait(fogoes) antes de cozinhar. Se os dois fogões estiverem ocupados, o terceiro cozinheiro (se houvesse) esperaria. Depois de terminar, sem_post(fogoes) libera o fogão para outro usar.
Mutex é um semáforo(1) — só 1 thread entra por vez. Com 2 fogões e 2 cozinheiros, eu queria que ambos cozinhassem em paralelo. Mutex bloquearia o segundo cozinheiro desnecessariamente. Semáforo(2) permite exatamente 2 threads simultâneas.
A barreira é um ponto onde N threads precisam se encontrar antes de qualquer uma continuar. Usei para sincronizar as rodadas de atendimento:
Mesa 1 termina pedidos → chega na barreira → espera
Mesa 2 termina pedidos → chega na barreira → espera
...
Mesa 7 termina pedidos → chega na barreira → LIBERA TODAS
Só quando todas as 7 mesas e a thread main chegam é que o sistema libera todo mundo para iniciar a próxima rodada.
Como fazer as threads de garçom e cozinheiro pararem quando não há mais pedidos? A solução mais simples é o poison pill (pílula envenenada): injetar na fila um pedido especial com id = -1. Cada thread, ao receber esse pedido, sabe que deve encerrar.
if (p.id == PEDIDO_FIM) break; // encerra o loopIsso é muito melhor do que usar flags volatile (que em C11 é comportamento indefinido entre threads sem sincronização adequada) ou sem_timedwait com timeout (que adiciona atraso desnecessário de 1s no encerramento).
O padrão produtor-consumidor com semáforos é a solução clássica para esse tipo de problema. A alternativa seria usar apenas mutex com pthread_cond_wait, o que funciona, mas exige um loop de verificação explícito e é mais verboso. O semáforo already encapsula exatamente a semântica que precisamos: "espere até ter permissão para prosseguir".
A regra mais importante aqui, que aprendi na prática: nunca chame sem_wait enquanto segura um mutex. Se você fizer isso, a thread pode bloquear segurando o mutex e nenhuma outra thread consegue acessar a fila — deadlock na certa. Por isso o sem_wait sempre vem antes do lock, e o sem_post sempre vem depois do unlock.
A escolha aqui foi direta. Com 2 fogões idênticos, qualquer cozinheiro pode usar qualquer um. Um mutex só permitiria 1 cozinheiro por vez, desperdiçando metade da capacidade. O semáforo(2) reflete exatamente a realidade: há 2 unidades do recurso disponíveis.
Spinlock (loop de espera ocupada) foi descartado imediatamente: TEMPO_PREPARO = 2 segundos. Um spinlock manteria a CPU em 100% durante esses 2 segundos esperando fogão. O semáforo POSIX coloca a thread para dormir no kernel via futex, liberando a CPU completamente.
A barreira é o primitivo certo para o padrão "todos chegam, todos saem juntos". Implementar isso com semáforos exigiria: um contador com mutex, um semáforo invertido para bloquear quem chegou cedo, e lógica para liberar todos no final — mais código, mais chance de bug. A barreira POSIX resolve tudo com uma linha: pthread_barrier_wait().
A thread main participa da barreira com propósito claro: ela imprime o separador de rodada exatamente quando todas as mesas terminaram, nem antes nem depois.
As estatísticas acumulam valores de múltiplos campos (pedidos_prontos, soma_espera, soma_preparo). Tipos atômicos C11 (_Atomic) funcionam campo a campo — não garantem que os três campos sejam atualizados juntos. O mutex garante que nenhuma thread lê estatísticas parcialmente atualizadas.
| Ideia | Por que descartei |
|---|---|
sem_timedwait + flag volatile para encerramento |
volatile não fornece memory ordering entre threads em C11 (comportamento indefinido). Além disso, o encerramento teria atraso de até 1s por timeout. |
| Spinlock para fogões | TEMPO_PREPARO = 2s: manteria a CPU em 100% durante toda a espera. |
pthread_cond_wait nas filas |
Funciona, mas é mais verboso que semáforo e exige loop de verificação. Semáforo é mais direto para produtor-consumidor. |
| Mutex nos fogões | Semáforo(1) = mutex: bloquearia o 2º cozinheiro mesmo com fogão disponível. |
Flag global sistema_ativo |
Dependeria de volatile ou de memory barriers manuais — poison pill é mais simples e mais correto. |
1. Encerramento sem deadlock
Esse foi o problema mais difícil. Na primeira versão, usei sem_timedwait com timeout de 1 segundo e uma flag volatile int sistema_ativo. O código até funcionava no Linux x86, mas estava tecnicamente errado: ler e escrever uma variável compartilhada sem sincronização é comportamento indefinido em C11 (x86 tem modelo de memória forte e raramente falha na prática, mas ARM e RISC-V podem falhar). A solução com poison pill elimina completamente o problema: cada thread espera normalmente com sem_wait, recebe sua pill e sai. Determinístico e correto.
2. usleep causando erro de compilação
Com _POSIX_C_SOURCE=200809L definido, o compilador não declara usleep porque ela foi removida do padrão POSIX.1-2008. Substituí por ms_sleep(), uma função própria que usa nanosleep(), que é o substituto correto.
3. IDs de pedidos duplicados
Sem mutex em ++stats.proximo_id, duas threads podiam ler o mesmo valor e gerar IDs iguais. A operação parece atômica em C, mas é compilada como três instruções de assembly (LOAD / ADD / STORE). O mutex em torno do incremento resolve.
4. Mensagens intercaladas no terminal
Sem controle, duas threads imprimindo ao mesmo tempo produzem saída ilegível. Criei log_ts() com um mutex interno (mtx_log) que serializa toda a sequência printf + fflush.
O projeto segue a arquitetura de 3 arquivos exigida:
src/
├── sincronizacao.h → tipos, constantes, extern das variáveis e protótipos
├── sincronizacao.c → definição das variáveis, init/destroy, operações de fila,
│ funções utilitárias (log_ts, ms_sleep, elapsed...)
└── main.c → implementação das threads e main()
Cada arquivo compila independentemente. main.c não sabe como a fila funciona internamente — só usa a API do .h. Isso facilita testar e modificar partes do código sem afetar o resto.
╔══════════════════════════════════════════════════════════════╗
║ SIMULADOR DE RESTAURANTE CONCORRENTE ║
║ LPII — Exercício Prático 2 — Matrícula: xxxxxx7380 ║
╠══════════════════════════════════════════════════════════════╣
║ Mesas (clientes) : 7 ║
║ Garçons : 3 ║
║ Cozinheiros : 2 ║
║ Fogões : 2 ║
║ Tempo de preparo : 2 s ║
║ Pedidos/mesa: 3 | Rodadas: 2 | Total: 42 ║
╚══════════════════════════════════════════════════════════════╝
[SETUP] Criando 2 cozinheiros...
[ 0.001s] Cozinheiro 1: aguardando pedidos...
[SETUP] Criando 3 garçons...
[ 0.002s] Cozinheiro 2: aguardando pedidos...
[ 0.002s] Garçom 1: aguardando pedidos...
[ 0.002s] Garçom 2: aguardando pedidos...
[ 0.002s] Garçom 3: aguardando pedidos...
[SETUP] Criando 7 mesas (clientes)...
[ 0.053s] Mesa 1 | Rodada 1/2: iniciando 3 pedidos
[ 0.053s] Mesa 1 | Pedido #01 | Prato 04 | Rod.1 -> fila garçons
[ 0.053s] Garçom 1: coletou Pedido #01 da Mesa 1 (Prato 04)
[ 0.053s] Mesa 2 | Rodada 1/2: iniciando 3 pedidos
[ 0.053s] Mesa 2 | Pedido #02 | Prato 07 | Rod.1 -> fila garçons
[ 0.053s] Garçom 2: coletou Pedido #02 da Mesa 2 (Prato 07)
[ 0.053s] Mesa 3 | Rodada 1/2: iniciando 3 pedidos
[ 0.053s] Mesa 3 | Pedido #03 | Prato 10 | Rod.1 -> fila garçons
[ 0.053s] Garçom 3: coletou Pedido #03 da Mesa 3 (Prato 10)
...
[ 0.173s] Cozinheiro 1: *** COZINHANDO *** Pedido #01 | Prato 04 | 2s
[ 0.213s] Cozinheiro 2: *** COZINHANDO *** Pedido #02 | Prato 07 | 2s
======== Rodada 1/2 concluída ========
[ 0.398s] Mesa 7 | Rodada 2/2: iniciando 3 pedidos
...
[ 2.173s] Cozinheiro 1: Pedido #01 PRONTO! [preparo: 2.00s]
======== Rodada 2/2 concluída ========
[ 0.742s] === Todos os clientes encerraram. Injetando pills nos garçons... ===
...
[ 42.230s] Cozinheiro 2: ENCERRADO (21 pedidos preparados)
O log completo está em resultados/log_execucao.txt.
| Métrica | Valor |
|---|---|
| Tempo total de simulação | 42.23 s |
| Pedidos criados pelos clientes | 42 / 42 |
| Pedidos entregues pelos garçons | 42 / 42 |
| Pedidos preparados pelos cozinheiros | 42 / 42 |
| Throughput | 0.994 pedidos/s |
| Tempo médio de espera na fila | 13.93 s |
| Tempo médio de preparo no fogão | 2.00 s |
| Utilização dos cozinheiros | 99.5% |
| Integridade | ✅ 42/42 [OK] |
Sim, completamente. Vou explicar cada número:
Por que 42 segundos no total?
É matemática pura. A cozinha tem capacidade máxima de:
2 cozinheiros × (1 prato / 2s) = 1 prato por segundo
42 pedidos ÷ 1 prato/s = 42 segundos de cozimento
As mesas fazem todos os pedidos em menos de 1 segundo. Os garçons entregam todos em cerca de 2 segundos. Mas a cozinha, com apenas 2 fogões e 2 cozinheiros, leva 42 segundos para processar tudo. O gargalo é a cozinha — exatamente como num restaurante real.
Por que o tempo médio de espera na fila foi 13.93s?
Os primeiros pedidos entram na fila da cozinha quase imediatamente e esperam pouco. Mas os últimos pedidos ficam presos na fila por quase 20 segundos esperando os cozinheiros ficarem livres. A média de ~14s reflete essa distribuição crescente.
Por que a utilização dos cozinheiros foi 99.5%?
Isso confirma que o gargalo está nos cozinheiros. Eles ficaram ocupados praticamente o tempo todo (apenas 0.5% de ociosidade), enquanto garçons e mesas terminaram muito antes. Se eu quisesse reduzir o tempo total, precisaria de mais fogões ou mais cozinheiros — e não de mais garçons ou mais mesas.
Paralelismo verificado no log:
No log é possível ver que ambos os cozinheiros trabalham simultaneamente:
[ 0.173s] Cozinheiro 1: *** COZINHANDO *** Pedido #01 | Prato 04 | 2s
[ 0.213s] Cozinheiro 2: *** COZINHANDO *** Pedido #02 | Prato 07 | 2s
[ 2.173s] Cozinheiro 1: Pedido #01 PRONTO! ← paralelo real
[ 2.213s] Cozinheiro 2: Pedido #02 PRONTO! ← paralelo real
Os dois estão cozinhando ao mesmo tempo, com timestamps de conclusão próximos. O semáforo dos fogões funcionou corretamente.
Zero pedidos perdidos: todos os 42 pedidos passaram por todas as etapas — criação, entrega pelos garçons e preparo pelos cozinheiros — sem nenhuma perda ou duplicação.
Usei Claude (Anthropic) como ferramenta principal durante todo o desenvolvimento.
Usei a IA principalmente para três coisas:
Estruturar a arquitetura inicial. Sabia que precisava de threads, semáforos e mutex, mas não tinha clareza sobre como organizar os canais de comunicação. A IA ajudou a definir as duas filas separadas (mesas→garçons e garçons→cozinheiros) e o protocolo exato de produtor-consumidor.
Identificar erros que eu não teria encontrado facilmente. Esse foi o uso mais valioso. A IA apontou que volatile em C11 não fornece memory ordering entre threads — achei que seria suficiente. Também identificou que usleep() foi removido do POSIX.1-2008 e que isso causaria erro de compilação com as flags corretas.
Entender o padrão poison pill. Eu teria provavelmente ficado com sem_timedwait por ser a primeira solução que funcionou. A IA explicou por que a poison pill é mais correta e me convenceu a reescrever o encerramento.
Sim, dois que identifiquei:
Erro 1 — volatile como solução de sincronização. Na primeira versão do código a IA usou volatile int sistema_ativo e volatile int clientes_prontos sem nenhum mutex. Quando questionei, ela mesma reconheceu o problema: em C11, acessar uma variável compartilhada entre threads sem sincronização adequada é comportamento indefinido. O volatile impede otimizações do compilador, mas não garante ordenação de memória entre CPUs diferentes — em x86 costuma "funcionar" por coincidência, em ARM pode falhar. Identifiquei isso pesquisando sobre memory model em C11 após a IA mencionar o termo.
Erro 2 — sem_timedwait como mecanismo de encerramento. A primeira versão de encerramento usava timeout de 1 segundo e verificava três flags separadas. O problema é que as três leituras (clientes_prontos, fila.count, pedidos_entregues) eram feitas em locks separados — outra thread podia modificar um valor entre as leituras, criando uma janela de race condition. Além disso, o encerramento tinha delay desnecessário. Identifiquei isso pensando em "o que acontece se o garçom 1 ler fila_vazia=true e na mesma hora o garçom 2 inserir algo?" — e percebi que o check não era atômico.
O protocolo correto de produtor-consumidor com semáforos, especialmente a regra "nunca chamar sem_wait enquanto segura mutex" e por que isso causa deadlock. Sabia que havia um risco, mas não conseguia articular exatamente onde estava. A IA explicou passo a passo o que acontece no estado das threads quando essa ordem é invertida.
Também aprendi sobre o padrão poison pill como mecanismo de encerramento. É uma ideia elegante que nunca teria pensado sozinha — a solução natural seria uma flag global, que tem os problemas que discuti acima.
A IA gera código que compila e roda, mas não necessariamente explica por que cada decisão foi tomada. Tive que forçar isso ativamente perguntando: "por que semáforo counting e não mutex aqui?", "qual a diferença real entre spinlock e semáforo nesse contexto?", "por que a ordem dos sem_wait e mutex importa?".
O mais importante foi entender que código que funciona no ambiente de teste pode ter bugs de sincronização que só aparecem sob carga específica, em outra CPU ou em outra arquitetura. O primeiro código com volatile rodava perfeitamente em x86 Linux e provavelmente nunca teria falhado na prática — mas estava errado. Aprender a pensar sobre corretude formal (e não só "funciona no meu computador") foi algo que a IA não entregou pronto: veio da necessidade de entender por que o código anterior estava sendo reescrito.
Segui três estratégias:
1. Documentar antes de entregar. Cada função do código tem um bloco de comentário explicando o que faz, por que foi escolhida e quais alternativas foram descartadas. Qualquer parte que não consegui comentar com precisão, estudei novamente até conseguir.
2. Raciocinar sobre casos extremos. Perguntei a mim mesmo: "O que acontece se dois clientes gerarem pedidos ao mesmo tempo?", "O que acontece se os dois fogões estiverem ocupados quando um terceiro cozinheiro tentar usar?", "O que acontece se a main chamar injetar_poison antes de todos os pedidos reais terem sido inseridos?". Para cada um desses casos, rastreei o comportamento do código e confirmei que estava correto.
3. Verificar os timestamps do log. Analisando a saída, confirmei que: nenhum pedido é preparado antes de ser entregue, o tempo de preparo é sempre ~2.00s (exato), os dois cozinheiros aparecem trabalhando simultaneamente no log com timestamps próximos, e a contagem final bate (42/42 em todas as etapas).
LPII-E002-7380/
├── README.md ← este relatório
├── Makefile
├── src/
│ ├── sincronizacao.h ← tipos, constantes, extern e protótipos
│ ├── sincronizacao.c ← variáveis globais, init/destroy, utilitários
│ └── main.c ← threads (cliente, garçom, cozinheiro) e main()
└── resultados/
└── log_execucao.txt ← saída real da execução