Skip to content

Vitoria-Holanda/Concurrent-system-simulator---LPII

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Simulador de Restaurante Concorrente

LPII — Programação Concorrente | Exercício Prático 2 | UFPB — CI
Professor: Carlos Eduardo Batista


6.1 Identificação

Campo Informação
Matrícula xxxxxx7380
Cenário escolhido A — Restaurante Concorrente
Linguagem C (pthreads POSIX)

Cálculo dos Parâmetros

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

Como compilar e executar

# Compilar
make

# Executar (salva log em resultados/)
make run

# Limpar objetos
make clean

Compilaçã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.c

Antes de tudo: os conceitos de concorrência que usei

Antes 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.

O problema central: múltiplas threads acessando dados compartilhados

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.


Mutex (pthread_mutex_t) — o cadeado

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)

Semáforo counting (sem_t) — o contador de permissões

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 faz sem_wait(slots) antes de inserir — se a fila estiver cheia, ele espera.
  • sem_peds: representa quantos itens disponíveis existem. O consumidor faz sem_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.


Por que não usei mutex nos fogões?

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.


Barreira (pthread_barrier_t) — ponto de encontro

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.


Padrão Poison Pill — encerramento sem deadlock

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 loop

Isso é 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).


6.2 Decisões de Projeto

Escolha dos mecanismos de sincronização

Semáforo + Mutex para as filas

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.

Semáforo counting para os fogões

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.

Barreira para sincronizar rodadas

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.

Mutex para estatísticas

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.


Alternativas consideradas e descartadas

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.

Principais desafios e como resolvi

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.


Estrutura do código

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.


6.3 Resultados Experimentais

Log de execução (trecho)

╔══════════════════════════════════════════════════════════════╗
║       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.


Estatísticas coletadas

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]

Análise: o sistema se comportou como esperado?

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.


6.4 Reflexão sobre Uso de IA

Ferramentas usadas

Usei Claude (Anthropic) como ferramenta principal durante todo o desenvolvimento.


Para quais tarefas?

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.


A IA cometeu algum erro?

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 que aprendi que não saberia sem a IA?

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.


O que aprendi apesar da IA?

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.


Como validei que entendi o código?

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).


Estrutura do Projeto

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

About

Simulador de sistema concorrente para aplicação em restaurantes.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors