Semáforos e Monitores
Vídeo da aula estará disponível em breve
Semáforos: A Ideia de Dijkstra
Em 1965, Edsger Dijkstra propôs o semáforo: uma variável inteira acessada apenas por duas operações atômicas. O nome vem dos semáforos ferroviários que controlam o acesso a trechos de trilho compartilhados.
Semáforo S: variável inteira ≥ 0
wait(S) [também chamada P, proberen, "testar"]:
atomicamente {
enquanto S == 0: bloqueia (dorme)
S = S - 1
}
signal(S) [também chamada V, verhogen, "incrementar"]:
atomicamente {
S = S + 1
se algum processo está bloqueado em wait(S): acorda um
}
Invariante: S nunca fica negativo.
As operações são ATÔMICAS: não podem ser interrompidas no meio.
Semáforo Binário (Mutex)
Um semáforo inicializado com 1 funciona como um lock de exclusão mútua. É chamado de semáforo binário ou mutex porque seu valor oscila apenas entre 0 e 1.
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
sem_t mutex;
int contador = 0;
void* incrementar(void* arg) {
for (int i = 0; i < 1000000; i++) {
sem_wait(&mutex); // P(mutex): decrementa, bloqueia se 0
contador++; // seção crítica (protegida)
sem_post(&mutex); // V(mutex): incrementa, acorda quem espera
}
return NULL;
}
int main() {
sem_init(&mutex, 0, 1); // valor inicial = 1
pthread_t t1, t2;
pthread_create(&t1, NULL, incrementar, NULL);
pthread_create(&t2, NULL, incrementar, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Resultado: %d\n", contador);
// Agora SEMPRE imprime 2000000
sem_destroy(&mutex);
return 0;
}
Semáforo Contador
Um semáforo inicializado com N > 1 permite que até N processos acessem o recurso simultaneamente. É usado para controlar acesso a um pool de recursos idênticos.
#define MAX_CONEXOES 5
sem_t pool;
void init_pool() {
sem_init(&pool, 0, MAX_CONEXOES); // 5 conexões disponíveis
}
void usar_conexao() {
sem_wait(&pool); // adquire 1 conexão (bloqueia se todas em uso)
// ... usa a conexão ...
sem_post(&pool); // libera 1 conexão
}
/*
Estado do semáforo durante execução:
Início: S = 5
Thread A chama wait: S = 4 (A tem conexão)
Thread B chama wait: S = 3 (B tem conexão)
Thread C chama wait: S = 2
Thread D chama wait: S = 1
Thread E chama wait: S = 0
Thread F chama wait: F BLOQUEIA (S não pode ficar negativo)
Thread A chama post: S = 1, F é acordado → S = 0
*/
Problema Produtor-Consumidor
O produtor-consumidor (ou bounded buffer) é o problema clássico de sincronização: um produtor coloca itens em um buffer de tamanho fixo, e um consumidor retira itens. O produtor deve esperar se o buffer está cheio; o consumidor deve esperar se está vazio.
#define TAM_BUFFER 10
int buffer[TAM_BUFFER];
int in = 0, out = 0;
sem_t mutex; // exclusão mútua no buffer (init = 1)
sem_t vazio; // slots vazios disponíveis (init = TAM_BUFFER)
sem_t cheio; // slots cheios disponíveis (init = 0)
void* produtor(void* arg) {
while (1) {
int item = produzir_item();
sem_wait(&vazio); // espera slot vazio
sem_wait(&mutex); // entra na SC
buffer[in] = item;
in = (in + 1) % TAM_BUFFER;
sem_post(&mutex); // sai da SC
sem_post(&cheio); // sinaliza: +1 slot cheio
}
}
void* consumidor(void* arg) {
while (1) {
sem_wait(&cheio); // espera slot cheio
sem_wait(&mutex); // entra na SC
int item = buffer[out];
out = (out + 1) % TAM_BUFFER;
sem_post(&mutex); // sai da SC
sem_post(&vazio); // sinaliza: +1 slot vazio
consumir_item(item);
}
}
/*
ATENÇÃO à ORDEM dos waits!
sem_wait(&vazio) ANTES de sem_wait(&mutex)
Se inverter: deadlock quando buffer cheio!
Produtor trava mutex, tenta wait(vazio) → bloqueia
Consumidor tenta wait(mutex) → bloqueia
Ninguém libera ninguém.
*/
Problema Leitores-Escritores
Múltiplos leitores podem acessar dados simultaneamente (leituras não conflitam), mas escritores precisam de acesso exclusivo. Este problema modela cenários como bancos de dados onde SELECTs são frequentes e UPDATEs são raros.
sem_t rw_mutex; // exclusão mútua para escritores (init = 1)
sem_t mutex; // protege read_count (init = 1)
int read_count = 0; // número de leitores ativos
void* leitor(void* arg) {
sem_wait(&mutex);
read_count++;
if (read_count == 1) // primeiro leitor trava o escritor
sem_wait(&rw_mutex);
sem_post(&mutex);
// LEITURA (múltiplos leitores podem estar aqui)
sem_wait(&mutex);
read_count--;
if (read_count == 0) // último leitor libera o escritor
sem_post(&rw_mutex);
sem_post(&mutex);
}
void* escritor(void* arg) {
sem_wait(&rw_mutex); // acesso exclusivo
// ESCRITA (apenas este escritor está aqui)
sem_post(&rw_mutex);
}
/*
Problema: starvation de escritores!
Se leitores chegam continuamente, read_count nunca chega a 0,
e o escritor nunca consegue entrar.
Solução: variantes com prioridade para escritores (Silberschatz cap.7)
*/
Jantar dos Filósofos
Cinco filósofos sentam ao redor de uma mesa circular. Entre cada par de filósofos há um garfo (hashii). Para comer, cada filósofo precisa de dois garfos (o da esquerda e o da direita). Este problema ilustra os perigos de deadlock.
[F0]
g4 / \ g0
[F4] [F1]
g3 | | g1
[F3] [F2]
\ g2 /
F0..F4 = filósofos
g0..g4 = garfos (um entre cada par)
Fi precisa de gi e g((i+1)%5)
Algoritmo ingênuo (DEADLOCK):
Filósofo i:
while (1) {
pensar();
pegar(garfo[i]); // pega esquerda
pegar(garfo[(i+1) % 5]); // pega direita
comer();
largar(garfo[i]);
largar(garfo[(i+1) % 5]);
}
Se todos pegam o garfo da esquerda ao mesmo tempo:
F0 tem g0, F1 tem g1, F2 tem g2, F3 tem g3, F4 tem g4
Todos esperam o garfo da direita → DEADLOCK!
sem_t garfo[5]; // cada garfo é um semáforo (init = 1)
void* filosofo(void* arg) {
int id = *(int*)arg;
int esq = id;
int dir = (id + 1) % 5;
while (1) {
pensar();
// Solução: filósofo par pega esquerda primeiro,
// filósofo ímpar pega direita primeiro.
// Quebra a circularidade → sem deadlock.
if (id % 2 == 0) {
sem_wait(&garfo[esq]);
sem_wait(&garfo[dir]);
} else {
sem_wait(&garfo[dir]);
sem_wait(&garfo[esq]);
}
comer();
sem_post(&garfo[esq]);
sem_post(&garfo[dir]);
}
}
/*
Outras soluções válidas:
1. Limitar a 4 filósofos sentados (semáforo contador init=4)
2. Pegar ambos os garfos atomicamente (com mutex global)
3. Solução de Chandy/Misra (garfos com prioridade)
*/
Monitores
Semáforos são poderosos mas perigosos: trocar a ordem de wait e signal causa deadlock ou viola exclusão mútua. Monitores (Hoare, 1974; Hansen, 1975) encapsulam dados compartilhados e operações em um módulo com exclusão mútua automática.
┌──────────────────────────────────────┐
│ MONITOR BoundedBuffer │
│ │
│ dados compartilhados: │
│ buffer[N], count, in, out │ ← acessíveis apenas
│ │ via procedimentos
│ condition variables: │ do monitor
│ notFull, notEmpty │
│ │
│ procedure insert(item): │
│ while count == N: │
│ notFull.wait() ← bloqueia │
│ buffer[in] = item │
│ in = (in+1) % N │
│ count++ │
│ notEmpty.signal() ← acorda │
│ │
│ procedure remove() → item: │
│ while count == 0: │
│ notEmpty.wait() │
│ item = buffer[out] │
│ out = (out+1) % N │
│ count-- │
│ notFull.signal() │
│ return item │
│ │
│ ┌──────────────────────────┐ │
│ │ Fila de entrada │ │
│ │ (threads esperando para │ │
│ │ entrar no monitor) │ │
│ └──────────────────────────┘ │
└──────────────────────────────────────┘
Regra fundamental:
Apenas UMA thread pode estar executando dentro do monitor.
As outras ficam na fila de entrada.
Não precisa de lock explícito — o monitor garante isso.
Variáveis de Condição
Dentro de um monitor, variáveis de condição (condition variables) permitem que uma thread espere por uma condição específica sem sair do monitor. São diferentes de semáforos: não têm valor, apenas filas de espera.
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t nao_vazio = PTHREAD_COND_INITIALIZER;
pthread_cond_t nao_cheio = PTHREAD_COND_INITIALIZER;
int buffer[10], count = 0, in = 0, out = 0;
void inserir(int item) {
pthread_mutex_lock(&mutex);
while (count == 10) // WHILE, não IF! (spurious wakeup)
pthread_cond_wait(&nao_cheio, &mutex);
// wait() atomicamente: libera mutex + bloqueia
// quando acorda: readquire mutex automaticamente
buffer[in] = item;
in = (in + 1) % 10;
count++;
pthread_cond_signal(&nao_vazio); // acorda 1 consumidor
pthread_mutex_unlock(&mutex);
}
int remover() {
pthread_mutex_lock(&mutex);
while (count == 0)
pthread_cond_wait(&nao_vazio, &mutex);
int item = buffer[out];
out = (out + 1) % 10;
count--;
pthread_cond_signal(&nao_cheio); // acorda 1 produtor
pthread_mutex_unlock(&mutex);
return item;
}
/*
Semáforo vs Condition Variable:
┌────────────────────┬─────────────────────────────┐
│ Semáforo │ Condition Variable │
├────────────────────┼─────────────────────────────┤
│ Tem valor (inteiro)│ Não tem valor (só fila) │
│ signal() acumula │ signal() perde-se se ninguém │
│ │ está esperando │
│ wait() pode não │ wait() sempre bloqueia │
│ bloquear (se S>0) │ │
│ Independente │ Precisa de mutex associado │
└────────────────────┴─────────────────────────────┘
*/
while e não if?
Em pthreads, pthread_cond_wait pode retornar sem que a condição tenha sido sinalizada (spurious wakeup). Além disso, outro thread pode ter consumido o item entre o signal e o retorno do wait. Sempre use while para re-verificar a condição após acordar.
Mutex vs Semáforo vs Monitor
┌─────────────┬────────────────┬──────────────────┬─────────────────────┐
│ │ Mutex │ Semáforo │ Monitor │
├─────────────┼────────────────┼──────────────────┼─────────────────────┤
│ Propósito │ Exclusão mútua │ Sincronização + │ Abstração completa │
│ │ │ controle de │ (dados + operações) │
│ │ │ recursos │ │
├─────────────┼────────────────┼──────────────────┼─────────────────────┤
│ Ownership │ Sim (quem │ Não (qualquer │ Implícito │
│ │ travou destrava│ um pode signal) │ │
├─────────────┼────────────────┼──────────────────┼─────────────────────┤
│ Valores │ 0 ou 1 │ 0 a N │ N/A (não tem valor) │
├─────────────┼────────────────┼──────────────────┼─────────────────────┤
│ Risco │ Baixo │ Alto (ordem │ Baixo (automático) │
│ │ │ errada = bug) │ │
├─────────────┼────────────────┼──────────────────┼─────────────────────┤
│ Em C/POSIX │ pthread_mutex │ sem_t │ mutex + cond_var │
├─────────────┼────────────────┼──────────────────┼─────────────────────┤
│ Em Java │ synchronized │ Semaphore │ synchronized block │
│ │ (implícito) │ (java.util) │ + wait/notify │
└─────────────┴────────────────┴──────────────────┴─────────────────────┘
No harness.os
O padrão produtor-consumidor aparece naturalmente no harness.os: agentes produzem eventos, decisões e aprendizados; sessões futuras consumem esses dados via handoffs. O Postgres age como o buffer limitado, e suas transações implementam monitores em nível de banco de dados:
Produtor-Consumidor no harness.os:
Produtores: Buffer (Postgres):
┌─────────┐ ┌──────────────────┐
│ Agente A │─── log_decision ──→│ │
│ Agente B │─── log_learning ──→│ Neon Postgres │
│ Agente C │─── end_session ──→│ (MVCC = monitor)│
└─────────┘ └────────┬─────────┘
│
Consumidores: │
┌─────────────┐ │
│ Próxima │←── start_session ──────┘
│ sessão │←── get_knowledge ──────┘
│ (handoff) │←── get_rules ──────────┘
└─────────────┘
Postgres resolve tudo:
- INSERT não conflita entre agentes (como sem_post)
- SELECT é lock-free via MVCC (como leitores no RW problem)
- UPDATE na mesma row: serialização automática (como mutex)
- Advisory locks: semáforo explícito quando necessário
SELECT pg_advisory_lock(42); -- wait(S)
SELECT pg_advisory_unlock(42); -- signal(S)
Exercícios
- No produtor-consumidor com semáforos, o que acontece se trocarmos a ordem para
sem_wait(&mutex)antes desem_wait(&vazio)no produtor? Desenhe o cenário de deadlock passo a passo. - Implemente o produtor-consumidor completo usando
pthread_mutexepthread_cond(monitores POSIX). Teste com 3 produtores e 2 consumidores. - No problema leitores-escritores, proponha uma solução que evite starvation de escritores. Dica: use um semáforo extra para sinalizar que um escritor está esperando.
- Na solução dos filósofos com semáforos, prove que a estratégia "par pega esquerda primeiro, ímpar pega direita primeiro" elimina o ciclo de espera circular.
Resumo
Verifique seu entendimento
No produtor-consumidor com semáforos, qual é a consequência de inverter sem_wait(&mutex) e sem_wait(&vazio) no produtor?
- Semáforos são variáveis inteiras com operações atômicas wait (P) e signal (V), propostas por Dijkstra
- Semáforo binário (mutex): exclusão mútua; semáforo contador: controle de pool de recursos
- Produtor-consumidor: 3 semáforos (mutex, vazio, cheio); a ordem dos waits é crítica
- Leitores-escritores: leituras concorrentes, escritas exclusivas; risco de starvation
- Jantar dos filósofos: ilustra deadlock; solução por quebra de simetria
- Monitores encapsulam dados + operações com exclusão mútua automática; condition variables para espera