College Online
0%

Semáforos e Monitores

Módulo 6 · Aula 2 ~30 min de leitura Nível: Intermediário

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.

Diagrama — Operações de semáforo
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.

C — Mutex com semáforo (POSIX)
#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.

C — Pool de conexões com semáforo contador
#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.

C — Produtor-Consumidor com semáforos
#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.

C — Leitores-Escritores (prioridade para leitores)
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.

Diagrama — Jantar dos Filósofos
         [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!
C — Solução: quebrar a simetria
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.

Diagrama — Estrutura de um monitor
┌──────────────────────────────────────┐
│  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.

C — Condition variables (POSIX pthreads)
#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   │
 └────────────────────┴─────────────────────────────┘
*/
i
Por que 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

Comparação de mecanismos de sincronização
┌─────────────┬────────────────┬──────────────────┬─────────────────────┐
│             │ 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:

Diagrama — Sincronização no harness.os
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

  1. No produtor-consumidor com semáforos, o que acontece se trocarmos a ordem para sem_wait(&mutex) antes de sem_wait(&vazio) no produtor? Desenhe o cenário de deadlock passo a passo.
  2. Implemente o produtor-consumidor completo usando pthread_mutex e pthread_cond (monitores POSIX). Teste com 3 produtores e 2 consumidores.
  3. 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.
  4. 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?

  • O buffer pode ser corrompido por acesso simultâneo
  • Pode ocorrer deadlock: o produtor trava o mutex e bloqueia esperando slot vazio, impedindo o consumidor de liberar slots
  • Os itens são consumidos na ordem errada (LIFO em vez de FIFO)
  • Nenhuma consequência, a ordem dos waits é irrelevante