Pular para o conteúdo principal

Aula 06 — Sincronização: Mutex, Semáforos e Monitores

Objetivos​

Ao final desta aula você deve ser capaz de:

  • Explicar o lock mutex como ferramenta de exclusão mútua e implementar exclusão mútua entre processos com semáforos POSIX.
  • Definir semáforo, distinguir semáforo binário de contador e implementar ambos os usos com wait() / signal().
  • Usar semáforos para sincronização de ordem (forçar S2 após S1) e para controle de pool de recursos.
  • Identificar erros clássicos de uso de semáforos (inversão de wait/signal, omissão) e seus efeitos.
  • Descrever o monitor como construtor de linguagem de alto nível e explicar o papel das variáveis de condição x.wait() / x.signal().
  • Comparar mutex, semáforo e monitor quanto a nível de abstração, expressividade e risco de erro.
  • Implementar o padrão fork-exec-wait com comunicação entre pai e filho usando variáveis de ambiente ou pipe.

Conteúdo​

Visão geral das ferramentas de sincronização​

A Aula 05 apresentou o problema da seção crítica e exigências de qualquer solução. Esta aula apresenta as ferramentas que os sistemas operacionais e linguagens fornecem para resolver esse problema na prática.


Lock Mutex​

O mutex (mutual exclusion lock) é a ferramenta mais simples: uma trava binária que um processo deve adquirir antes de entrar na seção crítica e liberar ao sair.

Semântica:

  • acquire(): se o mutex está disponível, adquire-o (agora indisponível) e prossegue. Se está indisponível, bloqueia o processo até que seja liberado.
  • release(): torna o mutex disponível e acorda um processo bloqueado (se houver).

Mutex entre processos com semáforos POSIX​

/* Executável — exclusão mútua entre processos com semáforo binário */
/* Compile: gcc -o contador contador.c -lpthread */
#include <sys/mman.h> /* mmap, MAP_SHARED, MAP_ANONYMOUS */
#include <sys/wait.h> /* wait */
#include <semaphore.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#define N_PROC 4
#define INCREMENTOS 100000

int main(void)
{
/* Variável compartilhada e semáforo na mesma região de memória */
long *contador = mmap(NULL, sizeof(long),
PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS, -1, 0);
sem_t *mtx = mmap(NULL, sizeof(sem_t),
PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS, -1, 0);
*contador = 0;
/* pshared=1: visível entre processos; valor=1: semáforo binário */
sem_init(mtx, 1, 1);

for (int i = 0; i < N_PROC; i++) {
if (fork() == 0) {
for (int j = 0; j < INCREMENTOS; j++) {
sem_wait(mtx); /* acquire — bloqueia se S = 0 */
(*contador)++;
sem_post(mtx); /* release */
}
exit(0);
}
}

for (int i = 0; i < N_PROC; i++) wait(NULL);

printf("Esperado: %ld\n", (long)N_PROC * INCREMENTOS);
printf("Obtido: %ld\n", *contador);
/* Sem sem_wait/sem_post: valores incorretos. Com eles: sempre correto. */
sem_destroy(mtx);
return 0;
}

Atividade prática rápida: remova as chamadas de sem_wait/sem_post e execute várias vezes. Observe que o resultado varia entre execuções — a condição de corrida em ação. Reinsira as chamadas e confirme que o resultado é sempre determinístico.

Spinlock vs. mutex bloqueante:

CaracterísticaSpinlockMutex bloqueante
EsperaBusy waiting (gira na CPU)Bloqueia o processo (suspende)
Overhead de espera curtaBaixo (sem troca de contexto)Alto (context switch)
Overhead de espera longaAlto (CPU desperdiçada)Baixo (não consome CPU)
Uso típicoKernel, seções de microsegundosAplicações, seções longas

Semáforos​

Um semáforo S é uma variável inteira acessada apenas por duas operações atômicas:

Notação histórica: wait() = P() = "proberen" (testar, em holandês). signal() = V() = "verhogen" (incrementar). Você verá as duas notações na literatura.

Quando S é negativo, |S| = número de processos bloqueados aguardando.

Semáforo Binário (mutex por semáforo)​

Inicializado em 1. Funciona como um mutex — garante exclusão mútua:

  • Se Pi entra primeiro: mutex = 1 → 0. Pi executa a SC.
  • Pj chega: mutex = 0 → −1. Pj bloqueia (|−1| = 1 processo esperando).
  • Pi sai e faz signal: mutex = −1 → 0. Pj é acordado e entra na SC.

Semáforo Contador (pool de recursos)​

Inicializado com o número de instâncias disponíveis do recurso. Controla acesso a pools:

Quando disponivel = 0: todos os recursos estão em uso. Novos processos bloqueiam até que um recurso seja devolvido.

Semáforo para sincronização de ordem​

Semáforos inicializados em 0 forçam que uma operação ocorra antes de outra — mesmo que os processos sejam concorrentes:

Uso prático: garantir que a thread de inicialização conclua antes que as threads de trabalho comecem.

Implementação com bloqueio real (sem busy waiting)​

A implementação do semáforo que bloqueia processos (em vez de spin) usa uma fila interna:

/* Somente leitura — estrutura interna do semáforo bloqueante */
typedef struct {
int value; /* valor do semáforo */
struct process *list; /* fila de processos bloqueados */
} semaphore;

void wait(semaphore *S) {
S->value--;
if (S->value < 0) {
/* insere este processo em S->list */
block(); /* suspende o processo atual */
}
}

void signal(semaphore *S) {
S->value++;
if (S->value <= 0) {
/* remove processo P de S->list */
wakeup(P); /* acorda P (move para fila de prontos) */
}
}

Observação importante: nessa implementação, o valor do semáforo pode ser negativo — o valor negativo indica quantos processos estão na fila de espera. Isso não ocorre na versão com busy waiting.

Erros clássicos com semáforos​

Pequenos erros de programação quebram toda a garantia de sincronização:

ErroCódigo erradoConsequência
Inversão de wait/signalsignal(mutex); SC; wait(mutex)Múltiplos processos na SC simultaneamente — violação de exclusão mútua
Wait duplicadowait(mutex); SC; wait(mutex)Deadlock — o processo bloqueia a si mesmo
Omitir signalwait(mutex); SC;Deadlock — outros processos ficam bloqueados para sempre
Omitir waitSC; signal(mutex)Sem exclusão mútua — condição de corrida

Esses erros são difíceis de detectar porque só se manifestam em interleavings específicos — podem não aparecer em testes normais.

Semáforos POSIX em C​

/* Executável — semáforo de ordenação entre processos (fork) */
/* Compile: gcc -o sem_ord sem_ord.c -lpthread */
#include <sys/mman.h>
#include <sys/wait.h>
#include <semaphore.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void) {
int *recurso = mmap(NULL, sizeof(int), PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS, -1, 0);
sem_t *pronto = mmap(NULL, sizeof(sem_t), PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS, -1, 0);
*recurso = 0;
sem_init(pronto, 1, 0); /* pshared=1, valor=0 → consumidor bloqueará */

if (fork() == 0) {
/* filho = produtor */
*recurso = 42;
printf("Produtor: recurso = %d\n", *recurso);
sem_post(pronto); /* signal() — acorda o pai (consumidor) */
exit(0);
}

/* pai = consumidor */
sem_wait(pronto); /* wait() — bloqueia até o filho sinalizar */
printf("Consumidor: recurso = %d\n", *recurso);
wait(NULL);
sem_destroy(pronto);
return 0;
}

API POSIX de semáforos:

FunçãoEquivalenteDescrição
sem_init(&s, pshared, val)—Inicializa semáforo sem nome; pshared=0: entre threads; pshared=1: entre processos (em mmap compartilhado)
sem_wait(&s)wait(S) / P(S)Decrementa; bloqueia se < 0
sem_post(&s)signal(S) / V(S)Incrementa; acorda processo se necessário
sem_destroy(&s)—Libera o semáforo
sem_open(nome,...)—Semáforo nomeado (entre processos distintos)

Monitores​

Semáforos são poderosos mas propensos a erros de programação. O monitor é um construtor de linguagem de alto nível que encapsula dados compartilhados e garante exclusão mútua automaticamente.

Propriedade fundamental do monitor: somente um processo por vez pode estar ativo dentro do monitor — a exclusão mútua é garantida pela linguagem/runtime, não pelo programador.

Estrutura de um monitor​

monitor NomeDoMonitor {
/* variáveis de estado compartilhadas */
int saldo;
condition pode_sacar;

/* operações — apenas uma executa por vez */
void depositar(int valor) {
saldo += valor;
pode_sacar.signal(); /* notifica quem está esperando */
}

void sacar(int valor) {
while (saldo < valor)
pode_sacar.wait(); /* bloqueia e LIBERA o monitor */
saldo -= valor;
}

/* código de inicialização */
NomeDoMonitor() { saldo = 0; }
}

Variáveis de condição​

Dentro do monitor, o programador pode declarar variáveis de condição para sincronização condicional:

condition x;

x.wait() — o processo bloqueia e LIBERA o monitor (outro pode entrar)
x.signal() — acorda UM processo bloqueado em x.wait() (se nenhum, não faz nada)

Diferença crítica entre x.signal() e signal(S) do semáforo:

  • signal(S) do semáforo sempre afeta o estado — incrementa S mesmo sem ninguém esperando.
  • x.signal() do monitor só tem efeito se há processo em x.wait() — caso contrário, é uma operação nula.

Monitores vs. Semáforos​

AspectoSemáforoMonitor
Exclusão mútuaManual (wait/signal)Automática (garantida pela linguagem)
Risco de erroAlto (inversões, omissões)Baixo (estrutura força o padrão correto)
ExpressividadeAlta (mais flexível)Alta (com variáveis de condição)
Suporte em linguagensC (POSIX), AssemblyJava (synchronized), C# (lock)
NívelSO/bibliotecaLinguagem de programação

Java — synchronized como monitor:

/* Somente leitura — monitor em Java com synchronized */
public class ContaBancaria {
private int saldo;

/* synchronized garante exclusão mútua automaticamente */
public synchronized void depositar(int valor) {
saldo += valor;
notifyAll(); /* equivalente a x.signal() */
}

public synchronized void sacar(int valor)
throws InterruptedException {
while (saldo < valor)
wait(); /* equivalente a x.wait() — libera o lock */
saldo -= valor;
}
}

Revisão: padrão Fork-Exec-Wait​

A ementa da Aula 06 inclui revisão de fork e fork-exec. O padrão completo é fundamental para criar processos que executam programas distintos:

/* Executável — padrão fork-exec-wait completo */
/* Compile: gcc -o forkexec forkexec.c */
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
pid_t pid;
int status;

pid = fork();

if (pid < 0) {
perror("fork");
exit(1);
}

if (pid == 0) {
/* ─── FILHO ─────────────────────────────────── */
/* execve substitui a imagem do filho por /bin/ls */
char *args[] = {"ls", "-la", "/tmp", NULL};
execvp("ls", args);
/* se chegou aqui, execvp falhou */
perror("execvp");
exit(1);
}

/* ─── PAI ──────────────────────────────────────── */
pid = waitpid(pid, &status, 0); /* aguarda o filho específico */

if (WIFEXITED(status))
printf("Filho terminou com código %d\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("Filho morto por sinal %d\n", WTERMSIG(status));

return 0;
}

Macros de waitpid:

MacroSignificado
WIFEXITED(status)Filho terminou normalmente com exit()
WEXITSTATUS(status)Código de saída (0–255)
WIFSIGNALED(status)Filho morto por sinal
WTERMSIG(status)Número do sinal que matou o filho

Atividade prática rápida: compile e execute o programa acima. Em seguida, modifique args para executar "grep" com algum padrão em /var/log. Observe que o processo pai recebe o código de saída do filho via waitpid.


Exercícios​

Questões dissertativas​

Q1

Explique a diferença entre um lock mutex e um semáforo binário. Eles são intercambiáveis? Em que situação um semáforo binário não deve ser usado no lugar de um mutex?

Q2

Dado um semáforo S inicializado em 3, descreva o valor de S e o estado de cada processo após a seguinte sequência: P1 faz wait(S); P2 faz wait(S); P3 faz wait(S); P4 faz wait(S); P5 faz signal(S).

Q3

Mostre como usar um semáforo para garantir que a operação B() só execute após A() ter sido concluída, mesmo que os processos PA e PB sejam iniciados em qualquer ordem.

Q4

Identifique o erro no código abaixo e explique a consequência. Como corrigi-lo? wait(mutex); seção crítica wait(mutex); /* deveria ser signal */

Q5

Explique o papel das variáveis de condição em um monitor. Qual a diferença semântica entre x.signal() de um monitor e signal(S) de um semáforo quando não há processos aguardando?

Q6

Explique como Java implementa o conceito de monitor através da palavra-chave synchronized. Qual é o papel de wait() e notifyAll() em um método synchronized?

Q7

Descreva o padrão fork-exec-wait. Por que o padrão correto separa fork() do exec() em vez de criar o processo diretamente com o novo programa?

Q8Difícil

Dois semáforos S e Q são inicializados em 1. O processo P0 executa wait(S); wait(Q); signal(S); signal(Q). O processo P1 executa wait(Q); wait(S); signal(Q); signal(S). Há risco de deadlock? Justifique e proponha correção.

Q9

Qual a vantagem do monitor sobre o semáforo do ponto de vista de segurança de programação? Dê um exemplo de erro com semáforos que o monitor previne estruturalmente.

Q10Difícil

Compare as três ferramentas de sincronização desta aula (mutex, semáforo, monitor) em uma tabela com os critérios: nível de abstração, quem garante exclusão mútua, uso de variáveis de condição, linguagens/APIs e principal risco.


Quiz de múltipla escolha​

Quiz10 questões

1. Um semáforo S é inicializado em 1. Após as operações wait(S); wait(S); signal(S), qual é o valor de S e quantos processos estão bloqueados?

  • a)S = 1, nenhum bloqueado
  • b)S = 0, nenhum bloqueado
  • c)S = -1, 1 processo bloqueado
  • d)S = 0, 1 processo bloqueado
  • e)S = -1, 0 processos bloqueados

2. Qual é a diferença semântica entre x.signal() de um monitor e signal(S) de um semáforo quando NENHUM processo está aguardando?

  • a)Nenhuma — ambos incrementam o contador interno
  • b)x.signal() acorda todos os processos; signal(S) acorda apenas um
  • c)x.signal() não tem efeito algum; signal(S) incrementa S e guarda o sinal para uso futuro
  • d)x.signal() bloqueia o processo; signal(S) o libera imediatamente
  • e)Ambos têm o mesmo efeito — são idênticos

3. Um programador usa 'signal(mutex)' antes da seção crítica (invertendo com wait). Qual a consequência?

  • a)Deadlock — o processo bloqueia a si mesmo
  • b)Starvation — o processo nunca entra na seção crítica
  • c)Múltiplos processos podem entrar na seção crítica simultaneamente, violando exclusão mútua
  • d)O semáforo fica em estado inválido e lança exceção
  • e)Nenhuma — signal antes de wait é permitido em semáforos

4. Para implementar um semáforo que controla acesso a um pool de 5 impressoras, qual deve ser o valor inicial do semáforo?

  • a)0 — para forçar todos a esperar inicialmente
  • b)1 — valor padrão de semáforos binários
  • c)5 — número de recursos disponíveis
  • d)-5 — para contagem negativa de espera
  • e)∞ — para permitir acesso ilimitado

5. Qual das afirmações sobre pthread_mutex_t em C está CORRETA?

  • a)Um thread pode fazer unlock em um mutex que outro thread travou
  • b)Um mutex pode ser travado múltiplas vezes pelo mesmo thread sem bloquear
  • c)O mesmo thread que fez lock deve fazer unlock — violação disso é comportamento indefinido
  • d)pthread_mutex_lock nunca bloqueia — é sempre não-bloqueante
  • e)Mutexes POSIX são automaticamente destruídos quando o processo termina

6. Em qual situação usar sem_post() (signal de semáforo POSIX) em vez de pthread_mutex_unlock() é a escolha CORRETA?

  • a)Quando o recurso protegido é um arquivo em disco
  • b)Quando um processo produtor quer sinalizar ao consumidor que um item foi produzido, sem que ambos compartilhem posse do mesmo lock
  • c)Quando apenas um thread precisa acessar o recurso por vez
  • d)Quando a seção crítica é muito longa (> 1 segundo)
  • e)Semáforos nunca devem ser usados no lugar de mutexes

7. No padrão fork-exec-wait, qual é o propósito de usar waitpid() em vez de wait() simples?

  • a)waitpid() é mais rápido que wait()
  • b)waitpid() permite especificar qual filho aguardar pelo PID, além de opções como WNOHANG para não bloquear
  • c)waitpid() funciona com exec(); wait() não funciona após exec()
  • d)waitpid() coleta o status de todos os filhos; wait() apenas do primeiro
  • e)waitpid() é necessário quando o filho usa semáforos

8. Qual a vantagem do monitor sobre o semáforo do ponto de vista de segurança de programação?

  • a)Monitores são mais rápidos que semáforos em todos os casos
  • b)Monitores permitem mais de um processo na seção crítica simultaneamente
  • c)A exclusão mútua é garantida estruturalmente pelo compilador/runtime — o programador não pode esquecer de liberar o lock
  • d)Monitores eliminam completamente a necessidade de variáveis de condição
  • e)Monitores são os únicos que funcionam em sistemas multiprocessador

9. Por que o padrão correto com variáveis de condição em monitores usa while() e não if() para verificar a condição antes de chamar x.wait()?

  • a)while() é mais eficiente que if() na JVM
  • b)if() causaria starvation enquanto while() evita
  • c)Após ser acordado por x.signal(), o thread deve reavaliar a condição pois outro thread pode ter mudado o estado antes de reacquirir o lock do monitor
  • d)x.wait() dentro de if() bloqueia o monitor permanentemente
  • e)Não há diferença — while() e if() são equivalentes nesse contexto

10. Um semáforo contador 'vagas' controla um estacionamento com 3 vagas. Qual é o valor de 'vagas' depois que: 2 carros entram, 1 sai, 3 entram, 1 sai?

  • a)vagas = 0, 0 carros esperando
  • b)vagas = -1, 1 carro esperando
  • c)vagas = 1, 0 carros esperando
  • d)vagas = -2, 2 carros esperando
  • e)vagas = 2, 0 carros esperando

Referências​

Principais (essenciais)​

  • SILBERSCHATZ, A.; GALVIN, P. B.; GAGNE, G. Fundamentos de Sistemas Operacionais. 9. ed. LTC, 2015.
    • §5.5 — Locks Mutex
    • §5.6 — Semáforos (uso, implementação, deadlock com semáforos)
    • §5.8 — Monitores (uso, variáveis de condição, implementação com semáforos)
    • §3.3 — Operações sobre Processos (fork-exec-wait)

Aprofundamento (opcionais)​

  • TANENBAUM, A. S. Sistemas Operacionais Modernos. 4. ed. Pearson, 2016. §2.3.4–2.3.7 — Semáforos, Mutexes, Monitores.
  • KERRISK, M. The Linux Programming Interface. No Starch Press, 2010. Cap. 30–31 (pthreads mutex e semáforos POSIX).
  • man 3 pthread_mutex_lock, man 3 sem_init, man 3 sem_wait — páginas de manual POSIX.

Recursos Complementares​

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

Formas para obtenção de exclusão mútua​

Thumbnail do vídeo: Solve Critical Section Problem with Busy Waiting
ENOpcional

Solve Critical Section Problem with Busy Waiting

Parul's E-Diary

Comunicação entre processos​

Thumbnail do vídeo: Inter Process Communication — Part 1/2
ENOpcional

Inter Process Communication — Part 1/2

Education 4u

Thumbnail do vídeo: Interprocess Communication
ENOpcional

Interprocess Communication

Neso Academy

Semáforos​

Thumbnail do vídeo: 19.2.2 Semaphores
ENOpcional

19.2.2 Semaphores

MIT OpenCourseWare

Thumbnail do vídeo: Semaphores
ENOpcional

Semaphores

Neso Academy

Monitores​

Thumbnail do vídeo: Monitors
ENOpcional

Monitors

Neso Academy