Pular para o conteúdo principal

Aula 10 — Problemas Clássicos de Sincronização

Ambiente sugerido

Os programas C desta aula compilam em Linux com gcc -lpthread. Execute-os para observar o comportamento concorrente em ação.

Objetivos​

Ao final desta aula você deve ser capaz de:

  • Descrever os três problemas clássicos de sincronização e identificar o padrão de cada um em aplicações reais.
  • Implementar a solução do Buffer Limitado com três semáforos (mutex, empty, full) e explicar o papel de cada um.
  • Explicar por que a ordem dos wait() importa no Produtor/Consumidor e o que acontece se invertida.
  • Analisar o problema dos Leitores-Gravadores nas suas duas variantes (favorece leitores / favorece gravadores) e identificar qual grupo pode sofrer starvation em cada uma.
  • Demonstrar o deadlock na solução ingênua do Jantar dos Filósofos e apresentar pelo menos três estratégias para evitá-lo.
  • Comparar a solução com semáforos e com monitor para o Jantar dos Filósofos, identificando a vantagem de cada abordagem.

Conteúdo​

Contexto: por que problemas clássicos?​

Os três problemas desta aula são mais do que exercícios acadêmicos — são padrões arquitetônicos que aparecem em sistemas reais:

ProblemaPadrão real
Buffer Limitado (Produtor/Consumidor)Filas de mensagens, pipelines de processamento, buffers de E/S, logs assíncronos
Leitores-GravadoresCaches, bancos de dados, sistemas de arquivos, configuração compartilhada
Jantar dos FilósofosAlocação de múltiplos recursos não-compartilháveis entre processos concorrentes

Cada um representa uma classe de problemas. Dominar as soluções aqui dá ferramentas para resolver variantes em qualquer domínio.


Problema 1: Buffer Limitado (Produtor/Consumidor)​

Descrição​

Um ou mais produtores geram itens e os inserem num buffer circular de tamanho N. Um ou mais consumidores retiram itens do buffer para processá-los.

Restrições a serem garantidas:

  1. O produtor não pode inserir se o buffer está cheio.
  2. O consumidor não pode retirar se o buffer está vazio.
  3. Apenas um processo acessa o buffer por vez (exclusão mútua).

Estrutura de dados​

Buffer circular com N slots:
┌───┬───┬───┬───┬───┐
│ │ │ │ │ │ capacity = N
└───┴───┴───┴───┴───┘
↑in ↑out

Três semáforos:

SemáforoValor inicialSignificado
mutex1Exclusão mútua no acesso ao buffer
emptyNNúmero de slots vazios (produtor pode inserir?)
full0Número de slots cheios (consumidor pode retirar?)

Solução com semáforos (pseudocódigo)​

Produtor: Consumidor:
───────────────────────── ────────────────────────
loop: loop:
produz item wait(full) ← espera haver item
wait(empty) ← espera slot wait(mutex)
wait(mutex) retira item do buffer
insere item no buffer signal(mutex)
signal(mutex) signal(empty) ← libera slot
signal(full) ← sinaliza item consome item

Por que wait(empty) vem ANTES de wait(mutex) no Produtor?

Se a ordem fosse invertida (wait(mutex) primeiro, depois wait(empty)):

  • Produtor adquire mutex, buffer está cheio → wait(empty) bloqueia com mutex em posse
  • Consumidor tenta wait(mutex) → bloqueia (mutex ocupado)
  • Deadlock: produtor aguarda consumidor liberar espaço, consumidor aguarda mutex que produtor detém

A ordem correta: semáforos de condição (empty/full) sempre antes do mutex.

Implementação em C com pthreads e semáforos POSIX​

/* Executável — Produtor/Consumidor com buffer limitado */
/* Compile: gcc -o prod_cons prod_cons.c -lpthread */
#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define N 5 /* tamanho do buffer */
#define N_PROD 2 /* número de produtores */
#define N_CONS 2 /* número de consumidores */
#define ITEMS_EACH 8 /* itens produzidos/consumidos por cada */

int buffer[N]; /* buffer circular compartilhado */
int in = 0; /* próxima posição de inserção */
int out = 0; /* próxima posição de retirada */

sem_t mutex; /* exclusão mútua no buffer */
sem_t empty; /* slots vazios disponíveis */
sem_t full; /* slots cheios disponíveis */

void *produtor(void *arg)
{
int id = *(int *)arg;
for (int i = 0; i < ITEMS_EACH; i++) {
int item = rand() % 100; /* produz item */

sem_wait(&empty); /* espera slot vazio */
sem_wait(&mutex); /* entra na SC */

buffer[in] = item;
printf("[P%d] inseriu %d na pos %d\n", id, item, in);
in = (in + 1) % N;

sem_post(&mutex); /* sai da SC */
sem_post(&full); /* sinaliza item */

usleep(rand() % 200000); /* simula produção */
}
return NULL;
}

void *consumidor(void *arg)
{
int id = *(int *)arg;
for (int i = 0; i < ITEMS_EACH; i++) {
sem_wait(&full); /* espera item */
sem_wait(&mutex); /* entra na SC */

int item = buffer[out];
printf("[C%d] retirou %d da pos %d\n", id, item, out);
out = (out + 1) % N;

sem_post(&mutex); /* sai da SC */
sem_post(&empty); /* sinaliza slot */

usleep(rand() % 300000); /* simula consumo */
}
return NULL;
}

int main(void)
{
pthread_t prod[N_PROD], cons[N_CONS];
int ids[N_PROD + N_CONS];

sem_init(&mutex, 0, 1);
sem_init(&empty, 0, N);
sem_init(&full, 0, 0);

for (int i = 0; i < N_PROD; i++) {
ids[i] = i;
pthread_create(&prod[i], NULL, produtor, &ids[i]);
}
for (int i = 0; i < N_CONS; i++) {
ids[N_PROD + i] = i;
pthread_create(&cons[i], NULL, consumidor, &ids[N_PROD + i]);
}
for (int i = 0; i < N_PROD; i++) pthread_join(prod[i], NULL);
for (int i = 0; i < N_CONS; i++) pthread_join(cons[i], NULL);

sem_destroy(&mutex);
sem_destroy(&empty);
sem_destroy(&full);
return 0;
}

Atividade prática rápida: altere N para 1 e observe como produtor e consumidor se revezam estritamente. Depois aumente para 10 e veja mais paralelismo.


Problema 2: Leitores-Gravadores​

Descrição​

Um banco de dados (ou qualquer recurso) é acessado por dois tipos de processos:

  • Leitores: podem ler concorrentemente — múltiplos leitores simultâneos são seguros.
  • Gravadores: precisam de acesso exclusivo — nenhum outro leitor ou gravador pode estar ativo.

Restrições:

  • Vários leitores podem ler simultaneamente.
  • Apenas um gravador por vez, com exclusividade total.

Primeira variante: favorece leitores​

Nenhum leitor espera a menos que um gravador já tenha obtido o acesso.

Dados compartilhados:
semaphore rw_mutex = 1; /* exclusão mútua gravadores ↔ leitores */
semaphore mutex = 1; /* protege read_count */
int read_count = 0; /* quantos leitores estão lendo agora */
Gravador: Leitor:
───────────────────── ─────────────────────────────────
loop: loop:
wait(rw_mutex) wait(mutex)
escreve read_count++
signal(rw_mutex) if read_count == 1:
wait(rw_mutex) ← 1º leitor bloqueia gravador
signal(mutex)

lê dados

wait(mutex)
read_count--
if read_count == 0:
signal(rw_mutex) ← último libera gravador
signal(mutex)

Lógica do leitor: o primeiro leitor que chega "bloqueia" gravadores adquirindo rw_mutex. Os leitores subsequentes simplesmente incrementam read_count sem precisar do rw_mutex. O último leitor a sair libera rw_mutex, permitindo que um gravador entre.

Problema desta variante: gravadores podem sofrer starvation — se leitores chegam continuamente, read_count nunca chega a 0, e o gravador espera indefinidamente.

Segunda variante: favorece gravadores​

Uma vez que um gravador está pronto, nenhum novo leitor pode começar a ler.

Problema desta variante: leitores podem sofrer starvation se gravadores chegam continuamente.

Tabela comparativa​

Aspecto1ª variante (favorece leitores)2ª variante (favorece gravadores)
Concorrência de leituraAltaMenor
Starvation de gravadoresPossívelNão
Starvation de leitoresNãoPossível
Uso típicoDados raramente modificados (config)Dados atualizados frequentemente

Read-write locks em POSIX:

/* Somente leitura — pthread_rwlock_t: abstração de nível mais alto */
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

/* Leitor */
pthread_rwlock_rdlock(&rwlock); /* múltiplos leitores simultâneos */
/* lê dados */
pthread_rwlock_unlock(&rwlock);

/* Gravador */
pthread_rwlock_wrlock(&rwlock); /* exclusão total */
/* escreve */
pthread_rwlock_unlock(&rwlock);

Problema 3: Jantar dos Filósofos​

Descrição​

Cinco filósofos sentam em torno de uma mesa circular. Entre cada par de filósofos adjacentes há um chopstick (palito). Para comer, um filósofo precisa dos dois chopsticks adjacentes — o da esquerda e o da direita. Quando não come, pensa.

F0
/ \
ch4 ch0
/ \
F4 ... F1
\ /
ch3 ch1
\ /
F3 — ch2 — F2

O problema: como garantir que filósofos possam comer sem deadlock e sem starvation?

Solução ingênua (com deadlock)​

/* Somente leitura — solução com deadlock */
semaphore chopstick[5] = {1, 1, 1, 1, 1};

/* Filósofo i */
do {
wait(chopstick[i]); /* pega o da esquerda */
wait(chopstick[(i+1) % 5]); /* pega o da direita */
/* come */
signal(chopstick[i]);
signal(chopstick[(i+1) % 5]);
/* pensa */
} while (true);

Cenário de deadlock: todos os 5 filósofos ficam com fome ao mesmo tempo e cada um pega seu chopstick esquerdo. Agora chopstick[0..4] = 0. Cada um tenta pegar o direito → todos bloqueiam. Espera circular fechada.

As quatro condições de Coffman estão presentes: exclusão mútua (chopstick), retenção e espera (tem um, espera outro), sem preempção, espera circular.

Três soluções para o deadlock​

Solução 1: Limite de 4 filósofos à mesa

Permitir no máximo 4 filósofos sentados simultaneamente. Com 4 filósofos e 5 chopsticks, pelo menos um sempre consegue pegar os dois — a espera circular é impossível.

/* Somente leitura — semáforo de capacidade */
semaphore cadeiras = 4; /* no máximo 4 filósofos à mesa */

wait(cadeiras);
wait(chopstick[i]);
wait(chopstick[(i+1) % 5]);
/* come */
signal(chopstick[i]);
signal(chopstick[(i+1) % 5]);
signal(cadeiras);

Solução 2: Pegar ambos os chopsticks atomicamente

Um filósofo só pega os chopsticks se ambos estiverem disponíveis — em seção crítica:

/* Somente leitura */
wait(mutex); /* entra na SC global */
wait(chopstick[i]);
wait(chopstick[(i+1) % 5]);
signal(mutex);
/* come */
signal(chopstick[i]);
signal(chopstick[(i+1) % 5]);

Problema: reduz concorrência — apenas um filósofo por vez pode tentar pegar chopsticks.

Solução 3: Ordenação assimétrica (quebra espera circular)

Filósofos ímpares pegam primeiro o chopstick esquerdo; filósofos pares pegam primeiro o direito. Isso quebra a simetria que causa a espera circular:

/* Somente leitura */
if (i % 2 == 0) {
wait(chopstick[(i+1) % 5]); /* par: direita primeiro */
wait(chopstick[i]);
} else {
wait(chopstick[i]); /* ímpar: esquerda primeiro */
wait(chopstick[(i+1) % 5]);
}

Solução com Monitor (livre de deadlock)​

A solução mais elegante usa um monitor com três estados por filósofo (THINKING, HUNGRY, EATING) e variáveis de condição:

/* Somente leitura — monitor DiningPhilosophers (pseudocódigo) */
monitor DiningPhilosophers {
enum {THINKING, HUNGRY, EATING} state[5];
condition self[5];

void pickup(int i) {
state[i] = HUNGRY;
test(i); /* tenta comer imediatamente */
if (state[i] != EATING)
self[i].wait(); /* espera ser liberado */
}

void putdown(int i) {
state[i] = THINKING;
test((i + 4) % 5); /* notifica vizinho esquerdo */
test((i + 1) % 5); /* notifica vizinho direito */
}

void test(int i) {
if (state[(i+4)%5] != EATING &&
state[i] == HUNGRY &&
state[(i+1)%5] != EATING) {
state[i] = EATING;
self[i].signal(); /* acorda filósofo i */
}
}

initialization_code() {
for (int i = 0; i < 5; i++) state[i] = THINKING;
}
}

Por que não há deadlock: um filósofo só muda para EATING se ambos os vizinhos não estão comendo — checagem atômica dentro do monitor. Nunca há situação em que um filósofo detém um recurso parcial esperando o outro.

Cuidado: esta solução é livre de deadlock mas não é livre de starvation — um filósofo pode morrer de fome se seus dois vizinhos se revezarem continuamente.

Implementação em C com pthreads​

/* Executável — Jantar dos Filósofos com mutex e variáveis de condição */
/* Compile: gcc -o filosofos filosofos.c -lpthread */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define N 5

typedef enum { THINKING, HUNGRY, EATING } Estado;

Estado state[N];
pthread_mutex_t mutex_monitor = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t self[N]; /* uma condição por filósofo */

void test(int i)
{
if (state[(i+4)%N] != EATING &&
state[i] == HUNGRY &&
state[(i+1)%N] != EATING) {
state[i] = EATING;
pthread_cond_signal(&self[i]); /* acorda filósofo i */
}
}

void pickup(int i)
{
pthread_mutex_lock(&mutex_monitor);
state[i] = HUNGRY;
printf("Filósofo %d está com fome\n", i);
test(i);
while (state[i] != EATING) /* verifica spurious wakeup */
pthread_cond_wait(&self[i], &mutex_monitor);
printf("Filósofo %d está COMENDO\n", i);
pthread_mutex_unlock(&mutex_monitor);
}

void putdown(int i)
{
pthread_mutex_lock(&mutex_monitor);
state[i] = THINKING;
printf("Filósofo %d está PENSANDO\n", i);
test((i+4)%N); /* notifica vizinhos */
test((i+1)%N);
pthread_mutex_unlock(&mutex_monitor);
}

void *filosofo(void *arg)
{
int i = *(int *)arg;
for (int r = 0; r < 3; r++) { /* 3 refeições cada */
usleep(rand() % 500000); /* pensa */
pickup(i);
usleep(rand() % 300000); /* come */
putdown(i);
}
return NULL;
}

int main(void)
{
pthread_t tid[N];
int ids[N];

for (int i = 0; i < N; i++) {
state[i] = THINKING;
pthread_cond_init(&self[i], NULL);
ids[i] = i;
}
for (int i = 0; i < N; i++)
pthread_create(&tid[i], NULL, filosofo, &ids[i]);
for (int i = 0; i < N; i++)
pthread_join(tid[i], NULL);

for (int i = 0; i < N; i++)
pthread_cond_destroy(&self[i]);
return 0;
}

Visão unificada dos três problemas​

Problema Recurso compartilhado Quem pode acessar Primitiva-chave
─────────────────────────────────────────────────────────────────────────────────
Buffer Limitado Buffer circular N slots 1 prod. ou 1 cons./vez empty, full, mutex
Leitores-Gravers Banco de dados N leitores OU 1 gravador rw_mutex, read_count
Jantar Filósofos 2 chopsticks por vez 1 filósofo p/ par vizinho state[], condição

Padrão comum: os três problemas precisam de dois tipos de sincronização simultâneos:

  1. Exclusão mútua (quem acessa o recurso)
  2. Sincronização condicional (quando pode acessar — buffer cheio, buffer vazio, vizinhos comendo)

Exercícios​

Questões dissertativas​

Q1

No problema do Buffer Limitado, explique o papel de cada um dos três semáforos (mutex, empty, full) e o que aconteceria se qualquer um deles fosse removido.

Q2

O que acontece se no Produtor a ordem for wait(mutex) antes de wait(empty) (invertida)? Demonstre com um cenário concreto.

Q3

Na solução do problema dos Leitores-Gravadores (1ª variante), explique por que o semáforo rw_mutex é adquirido apenas pelo primeiro leitor e liberado apenas pelo último. Qual o propósito do read_count?

Q4

Na solução ingênua do Jantar dos Filósofos, demonstre o deadlock com os 5 filósofos. Identifique as quatro condições de Coffman neste cenário.

Q5

Explique a solução assimétrica para o Jantar dos Filósofos. Por que quebrar a simetria elimina a espera circular?

Q6

Na solução com monitor para o Jantar dos Filósofos, por que a função test() é chamada tanto em pickup() quanto em putdown()? O que cada chamada verifica?

Q7

Compare as soluções do Jantar dos Filósofos com semáforos (solução ingênua + correção) e com monitor. Qual é mais fácil de raciocinar sobre corretude? Por quê?

Q8Difícil

A solução com monitor para o Jantar dos Filósofos é livre de deadlock mas não de starvation. Descreva um cenário onde um filósofo morreria de fome.

Q9

O problema dos Leitores-Gravadores pode aparecer em bancos de dados. Explique como pthread_rwlock_t resolve este problema na prática e quando você preferiria usá-lo no lugar de um mutex simples.

Q10Difícil

Trace a execução da solução com monitor para o Jantar dos Filósofos: F0 faz pickup(0), depois F1 faz pickup(1), depois F0 faz putdown(0). Quais transições de estado ocorrem? F1 consegue comer?


Quiz de múltipla escolha​

Quiz10 questões

1. No Buffer Limitado com semáforos, qual é o valor inicial correto de cada semáforo para um buffer de 5 slots?

  • a)mutex=0, empty=0, full=5
  • b)mutex=1, empty=5, full=0
  • c)mutex=1, empty=0, full=5
  • d)mutex=5, empty=1, full=0
  • e)mutex=1, empty=1, full=1

2. No Produtor/Consumidor, o Produtor executa wait(mutex) ANTES de wait(empty) com o buffer cheio. O que ocorre?

  • a)O produtor insere no buffer normalmente
  • b)O produtor libera o mutex e aguarda o buffer ter espaço
  • c)Deadlock — produtor bloqueia em wait(empty) com mutex em posse; consumidor não pode adquirir mutex para liberar espaço
  • d)Starvation — produtor nunca consegue inserir
  • e)O buffer circular é expandido automaticamente

3. Na solução dos Leitores-Gravadores (1ª variante), quantos leitores podem estar lendo simultaneamente enquanto nenhum gravador está ativo?

  • a)Apenas 1 — o mutex garante exclusão
  • b)Exatamente 2 — um para cada semáforo
  • c)N leitores ilimitados — múltiplos leitores são permitidos concorrentemente
  • d)O número de leitores definido na inicialização de rw_mutex
  • e)Apenas N/2 onde N é o tamanho do banco de dados

4. Na solução ingênua do Jantar dos Filósofos, todos os 5 filósofos ficam com fome e pegam seu chopstick esquerdo. Qual condição de deadlock é satisfeita pelo comportamento 'ninguém larga o chopstick que tem'?

  • a)Exclusão mútua
  • b)Retenção e espera
  • c)Inexistência de preempção
  • d)Espera circular
  • e)Starvation

5. A solução assimétrica do Jantar dos Filósofos (pares pegam direita primeiro, ímpares pegam esquerda) elimina qual condição de Coffman para prevenir deadlock?

  • a)Exclusão mútua — os chopsticks tornam-se compartilháveis
  • b)Retenção e espera — filósofos pegam ambos de uma vez
  • c)Inexistência de preempção — o SO pode tomar chopsticks
  • d)Espera circular — a assimetria garante que a cadeia de espera não fecha um ciclo
  • e)Não elimina nenhuma — apenas reduz a probabilidade

6. Na solução com monitor para o Jantar dos Filósofos, um filósofo i só pode passar para EATING se qual condição é verificada em test()?

  • a)state[i] == HUNGRY E state[i-1] == THINKING E state[i+1] == THINKING
  • b)state[i-1] != EATING E state[i] == HUNGRY E state[i+1] != EATING
  • c)state[i-1] == HUNGRY E state[i+1] == HUNGRY
  • d)read_count == 0 para ambos os vizinhos
  • e)O filósofo esperou mais de N segundos na fila

7. Qual é a função de pthread_rwlock_rdlock() versus pthread_rwlock_wrlock()?

  • a)rdlock bloqueia todos; wrlock permite leitura simultânea
  • b)rdlock permite múltiplos leitores simultâneos; wrlock garante acesso exclusivo ao escritor
  • c)Ambos garantem exclusão mútua total — a diferença é apenas semântica
  • d)rdlock é para threads; wrlock é para processos separados
  • e)rdlock é não-bloqueante; wrlock sempre bloqueia

8. A solução com monitor DiningPhilosophers é livre de deadlock. Qual aspecto do design garante isso?

  • a)O uso de variáveis de condição impede qualquer bloqueio
  • b)O filósofo só muda para EATING se ambos os vizinhos não estão EATING — verificação atômica dentro do monitor impede retenção parcial
  • c)O monitor usa apenas um semáforo para todos os filósofos
  • d)Os filósofos não podem ficar HUNGRY ao mesmo tempo
  • e)O monitor implementa timeout automático

9. No problema dos Leitores-Gravadores (1ª variante), gravadores podem sofrer starvation. Qual condição cria essa possibilidade?

  • a)Gravadores fazem wait(mutex) muito frequentemente
  • b)Se leitores chegam continuamente, read_count nunca chega a 0, e rw_mutex nunca é liberado para o gravador em espera
  • c)A primeira variante não permite que gravadores esperem
  • d)Gravadores têm prioridade menor que leitores no escalonador
  • e)O semáforo rw_mutex tem capacidade limitada de processos em fila

10. A solução do Buffer Limitado com semáforos suporta múltiplos produtores e múltiplos consumidores simultaneamente?

  • a)Não — a solução só funciona com exatamente 1 produtor e 1 consumidor
  • b)Sim — o semáforo mutex garante que apenas um processo (produtor OU consumidor) modifica o buffer por vez, e empty/full controlam as condições independentemente
  • c)Apenas múltiplos produtores — múltiplos consumidores precisam de solução diferente
  • d)Apenas se N (tamanho do buffer) for maior que o número de produtores + consumidores
  • e)Não — produtores e consumidores não podem compartilhar o mesmo mutex

Referências​

Principais (essenciais)​

  • SILBERSCHATZ, A.; GALVIN, P. B.; GAGNE, G. Fundamentos de Sistemas Operacionais. 9. ed. LTC, 2015.
    • §5.7.1 — Buffer Limitado (Produtor/Consumidor)
    • §5.7.2 — Leitores-Gravadores
    • §5.7.3 — Filósofos Comensais
    • §5.8.2 — Solução com Monitor para Filósofos

Aprofundamento (opcionais)​

  • TANENBAUM, A. S. Sistemas Operacionais Modernos. 4. ed. Pearson, 2016. §2.3.6 — Problemas clássicos de IPC.
  • KERRISK, M. The Linux Programming Interface. No Starch Press, 2010. Cap. 30 — Threads: sincronização.
  • man 3 pthread_rwlock_rdlock, man 3 pthread_rwlock_wrlock — referência POSIX de rwlocks.

Recursos Complementares​

Opcional — Vídeos para reforçar os conceitos desta aula.

Thumbnail do vídeo: Producer Consumer Problem Using Semaphores18:40
ENOpcional

Producer Consumer Problem Using Semaphores

Neso Academy

Solução completa do Buffer Limitado com três semáforos, passo a passo. Cobre §5.7.1 do Silberschatz.

Thumbnail do vídeo: Operating Systems — Full Course (25h)25:00:00
ENAprofundamento

Operating Systems — Full Course (25h)

freeCodeCamp.org

Para esta aula, foque nos módulos Classic Synchronization Problems e Dining Philosophers.